this repo has no description

hi

tbeseda.com 43c8502b

+2997
+1
.gitignore
··· 1 + node_modules
+313
.kiro/specs/tagged-string/design.md
··· 1 + # Design Document 2 + 3 + ## Overview 4 + 5 + The Tagged String library is a simple, zero-dependency TypeScript library that extracts tagged entities from strings. The design focuses on simplicity and minimal API surface while providing flexible configuration options. 6 + 7 + **Example Usage:** 8 + ```typescript 9 + import chalk from 'chalk'; // Example formatter (not a dependency of parser) 10 + 11 + // Define schema for known entity types with formatters 12 + const schema = { 13 + operation: { type: 'string', format: chalk.yellow }, 14 + stack: { type: 'string', format: chalk.blue }, 15 + changes: { type: 'number', format: (n) => chalk.green(n.toString()) }, 16 + create: 'number', // Can use shorthand without formatter 17 + update: 'number', 18 + destroy: 'number' 19 + }; 20 + 21 + const parser = new TaggedStringParser({ schema }); 22 + const result = parser.parse('[operation:OP-123] started with [changes:5] to [stack:ST-456]'); 23 + 24 + // Access parsed entities 25 + // result.entities: [ 26 + // { type: 'operation', value: 'OP-123', parsedValue: 'OP-123', formattedValue: '\x1b[33mOP-123\x1b[39m', inferredType: 'string', position: 0 }, 27 + // { type: 'changes', value: '5', parsedValue: 5, formattedValue: '\x1b[32m5\x1b[39m', inferredType: 'number', position: 32 }, 28 + // { type: 'stack', value: 'ST-456', parsedValue: 'ST-456', formattedValue: '\x1b[34mST-456\x1b[39m', inferredType: 'string', position: 48 } 29 + // ] 30 + 31 + // Get formatted message with all entities formatted 32 + console.log(result.format()); 33 + // Output: "\x1b[33mOP-123\x1b[39m started with \x1b[32m5\x1b[39m to \x1b[34mST-456\x1b[39m" 34 + 35 + // Unknown entity types are automatically inferred (no formatting) 36 + const result2 = parser.parse('[count:42] [enabled:true] [name:test]'); 37 + // result2.entities: [ 38 + // { type: 'count', value: '42', parsedValue: 42, formattedValue: '42', inferredType: 'number', position: 0 }, 39 + // { type: 'enabled', value: 'true', parsedValue: true, formattedValue: 'true', inferredType: 'boolean', position: 11 }, 40 + // { type: 'name', value: 'test', parsedValue: 'test', formattedValue: 'test', inferredType: 'string', position: 26 } 41 + // ] 42 + 43 + // Filter by type 44 + result.getEntitiesByType('operation'); // [{ type: 'operation', value: 'OP-123', ... }] 45 + 46 + // Get all types 47 + result.getAllTypes(); // ['operation', 'changes', 'stack'] 48 + ``` 49 + 50 + ## Architecture 51 + 52 + The parser follows a simple single-pass scanning architecture: 53 + 54 + 1. **Input**: Raw string + optional configuration 55 + 2. **Scanning**: Character-by-character traversal identifying tag boundaries 56 + 3. **Extraction**: Parse entity type and value from tag content 57 + 4. **Output**: Collection of parsed entities with original message preserved 58 + 59 + The design uses a state machine approach during scanning to track whether the parser is inside or outside a tag. 60 + 61 + ## Components and Interfaces 62 + 63 + ### Entity Interface 64 + ```typescript 65 + interface Entity { 66 + type: string; // Entity type name (e.g., 'operation', 'count') 67 + value: string; // Raw string value from tag 68 + parsedValue: string | number | boolean; // Typed value based on schema or inference 69 + formattedValue: string; // Formatted string (via formatter or toString of parsedValue) 70 + inferredType: 'string' | 'number' | 'boolean'; // The determined type 71 + position: number; // Character position in original string 72 + } 73 + ``` 74 + 75 + ### EntitySchema Type 76 + ```typescript 77 + type PrimitiveType = 'string' | 'number' | 'boolean'; 78 + 79 + interface EntityDefinition { 80 + type: PrimitiveType; 81 + format?: (value: any) => string; // Optional formatter function 82 + } 83 + 84 + type EntitySchema = Record<string, PrimitiveType | EntityDefinition>; 85 + ``` 86 + 87 + ### ParserConfig Interface 88 + ```typescript 89 + interface ParserConfig { 90 + openDelimiter?: string; // Default: '[' 91 + closeDelimiter?: string; // Default: ']' 92 + typeSeparator?: string; // Default: ':' 93 + schema?: EntitySchema; // Optional schema for known entity types 94 + } 95 + ``` 96 + 97 + ### ParseResult Interface 98 + ```typescript 99 + interface ParseResult { 100 + originalMessage: string; 101 + entities: Entity[]; 102 + 103 + // Utility methods 104 + getEntitiesByType(type: string): Entity[]; 105 + getAllTypes(): string[]; 106 + format(): string; // Returns message with all entities replaced by their formattedValue 107 + } 108 + ``` 109 + 110 + ### TaggedStringParser Class 111 + ```typescript 112 + class TaggedStringParser { 113 + constructor(config?: ParserConfig); 114 + parse(message: string): ParseResult; 115 + 116 + // Internal helper methods 117 + private parseValue(type: string, rawValue: string): { 118 + parsedValue: string | number | boolean, 119 + formattedValue: string, 120 + inferredType: 'string' | 'number' | 'boolean' 121 + }; 122 + private inferType(value: string): 'string' | 'number' | 'boolean'; 123 + private applyFormatter(type: string, parsedValue: any): string; 124 + } 125 + ``` 126 + 127 + ## Data Models 128 + 129 + ### Internal Scanner State 130 + The parser maintains minimal state during scanning: 131 + - Current position in string 132 + - Whether currently inside a tag 133 + - Current tag content buffer 134 + - Accumulated entities array 135 + 136 + ### Entity Representation 137 + Entities are plain objects with six properties: 138 + - `type`: The classification/name of the entity (e.g., 'operation', 'count') 139 + - `value`: The raw string value extracted from the tag 140 + - `parsedValue`: The typed value (string, number, or boolean) based on schema or inference 141 + - `formattedValue`: The string representation after applying formatter (or toString of parsedValue) 142 + - `inferredType`: The determined primitive type 143 + - `position`: Where in the original string the tag started (useful for debugging) 144 + 145 + ### Schema, Type Inference, and Formatting 146 + The parser supports three layers of entity processing: 147 + 148 + 1. **Schema-based Type Parsing (Known Entities)**: When a schema is provided and an entity type matches a schema key, the parser uses the schema's specified type to parse the value. 149 + 150 + 2. **Inference-based Type Parsing (Unknown Entities)**: When no schema is provided or an entity type is not in the schema, the parser automatically infers the type: 151 + - Numbers: Values matching `/^-?\d+(\.\d+)?$/` are parsed as numbers 152 + - Booleans: Values matching `true` or `false` (case-insensitive) are parsed as booleans 153 + - Strings: Everything else defaults to string type 154 + 155 + 3. **Formatting**: After parsing, entities can be formatted: 156 + - If a formatter function is provided in the schema, it's applied to the parsedValue 157 + - If no formatter is provided, the parsedValue is converted to string 158 + - The formattedValue is used when calling `result.format()` to reconstruct the message 159 + 160 + This three-layer approach allows users to: 161 + - Define expected entity types explicitly 162 + - Handle ad-hoc entities automatically 163 + - Apply custom formatting (colors, trimming, etc.) per entity type 164 + 165 + ## Error Handling 166 + 167 + The parser follows a lenient error handling strategy: 168 + 169 + 1. **Malformed Tags**: Skip and continue parsing 170 + - Unclosed tags: Ignore the incomplete tag 171 + - Missing type separator: Treat entire content as value with empty type 172 + - Empty tags: Skip entirely 173 + 174 + 2. **Invalid Configuration**: Throw errors during construction 175 + - Empty delimiters 176 + - Delimiter conflicts (open === close) 177 + - Multi-character delimiters that could cause ambiguity 178 + 179 + 3. **Edge Cases**: 180 + - Empty strings: Return empty entity array 181 + - Nested tags: Not supported, inner delimiters treated as literal characters 182 + - Escaped delimiters: Not supported in v1 (future enhancement) 183 + 184 + ## Testing Strategy 185 + 186 + ### Unit Tests 187 + Focus on core parsing logic: 188 + - Single entity extraction 189 + - Multiple entities in one message 190 + - Messages without entities 191 + - Malformed tag handling 192 + - Custom delimiter configuration 193 + - Entity ordering preservation 194 + - Type-based filtering 195 + - Schema-based type parsing 196 + - Automatic type inference for unknown entities 197 + - Number, boolean, and string type detection 198 + 199 + ### Test Structure 200 + ```typescript 201 + describe('TaggedStringParser', () => { 202 + describe('parse', () => { 203 + it('should extract single entity'); 204 + it('should extract multiple entities'); 205 + it('should handle messages without entities'); 206 + it('should skip malformed tags'); 207 + it('should preserve entity order'); 208 + }); 209 + 210 + describe('schema and type inference', () => { 211 + it('should parse known entities using schema'); 212 + it('should infer number type for numeric values'); 213 + it('should infer boolean type for true/false values'); 214 + it('should default to string type for other values'); 215 + it('should handle mixed known and unknown entities'); 216 + }); 217 + 218 + describe('configuration', () => { 219 + it('should use custom delimiters'); 220 + it('should throw on invalid config'); 221 + }); 222 + 223 + describe('ParseResult', () => { 224 + it('should filter entities by type'); 225 + it('should return all entity types'); 226 + }); 227 + }); 228 + ``` 229 + 230 + ## Real-World Examples 231 + 232 + Based on IaC system logging patterns: 233 + 234 + ```typescript 235 + // Operation lifecycle 236 + parser.parse('[operation:OP-123] started with [changes:5] to [stack:ST-456]'); 237 + parser.parse('[operation:OP-123] completed [changes:5] to [stack:ST-456]'); 238 + parser.parse('[operation:OP-123] failed: [reason:"Error message"]'); 239 + 240 + // Planning 241 + parser.parse('[blueprint:BP-123] planning for [stack:ST-456]'); 242 + parser.parse('[blueprint:BP-123] plan complete with [create:2] [update:3] [destroy:1] for [stack:ST-456]'); 243 + 244 + // Resource commands 245 + parser.parse('[action:create] executing for [resource:RS-123] [resourceName:"my-function"] [type:function]'); 246 + parser.parse('[action:create] completed for [resource:RS-123] [externalId:EXT-789]'); 247 + parser.parse('[action:create] failed for [resource:RS-123]: [error:"Error message"]'); 248 + 249 + // Resource type-specific 250 + parser.parse('[resourceType:function] creating [resourceName:"my-function"]'); 251 + parser.parse('[resourceType:database] updating [resourceName:"user-db"]'); 252 + ``` 253 + 254 + ### Handling Quoted Values 255 + Note that values may contain quotes (e.g., `[resourceName:"my-function"]`). The parser treats everything between the type separator and closing delimiter as the value, including quotes. This keeps the implementation simple while preserving the original data format. 256 + 257 + ## Runtime Environment 258 + 259 + ### Node.js v24 Native TypeScript Support 260 + The parser is designed to run directly with Node.js v24's native TypeScript execution: 261 + 262 + ```bash 263 + # Run directly without compilation 264 + node --experimental-strip-types parser.ts 265 + 266 + # Or with the simpler flag (Node v24+) 267 + node parser.ts 268 + ``` 269 + 270 + No build step or compilation to JavaScript is required. The parser can be developed and executed as pure TypeScript files. 271 + 272 + ## Implementation Notes 273 + 274 + ### Performance Considerations 275 + - Single-pass parsing: O(n) time complexity 276 + - Type inference uses simple string checks (no regex for numbers/booleans) 277 + - Minimal memory allocation (reuse buffers where possible) 278 + - Schema lookup is O(1) using object property access 279 + 280 + ### Design Decisions 281 + 282 + **Why character-by-character scanning?** 283 + - Simpler to understand and maintain 284 + - No regex complexity 285 + - Easier to handle edge cases 286 + - Predictable performance 287 + 288 + **Why lenient error handling?** 289 + - Strings should never break parsing 290 + - Partial data is better than no data 291 + - Aligns with robustness principle: "Be liberal in what you accept" 292 + 293 + **Why include position in Entity?** 294 + - Useful for debugging 295 + - Minimal overhead 296 + - Enables future features (e.g., highlighting in UI) 297 + 298 + **Why no nested tag support?** 299 + - Keeps implementation simple 300 + - Rare use case for strings 301 + - Can be added later if needed 302 + 303 + **Why both schema and inference?** 304 + - Schema provides explicit control for important entity types 305 + - Inference handles ad-hoc entities without configuration 306 + - Consumers get typed values for better formatting control 307 + - Balances flexibility with type safety 308 + 309 + **Why Node v24 without compilation?** 310 + - Faster development iteration (no build step) 311 + - Simpler project setup 312 + - Native TypeScript support is stable in Node v24 313 + - Reduces tooling complexity
+89
.kiro/specs/tagged-string/requirements.md
··· 1 + # Requirements Document 2 + 3 + ## Introduction 4 + 5 + The Tagged String library is a lightweight TypeScript string parsing library with zero dependencies. It extracts structured entity information from strings using a tag-based syntax. The parser identifies and extracts tagged entities (such as data models, counts, identifiers, and other structured data) from strings, making them programmatically accessible while maintaining the readability of the original message. 6 + 7 + ## Glossary 8 + 9 + - **Parser**: The system component that processes input strings and extracts tagged entities 10 + - **Entity**: A structured piece of information embedded in a string using tag syntax 11 + - **Tag**: A syntactic marker that identifies the start and type of an entity within a string 12 + - **String**: A human-readable string that may contain zero or more tagged entities 13 + - **Entity Type**: A classification label for an entity (e.g., model, count, identifier) 14 + - **Schema**: A user-defined specification that maps entity type names to their expected data types 15 + - **Known Entity**: An entity whose type is defined in the parser's schema 16 + - **Unknown Entity**: An entity whose type is not defined in the schema, requiring automatic type inference 17 + - **Primitive Type**: A basic data type (string, number, boolean) inferred from entity values 18 + 19 + ## Requirements 20 + 21 + ### Requirement 1 22 + 23 + **User Story:** As a developer, I want to parse tagged entities from strings, so that I can extract structured data while keeping strings human-readable 24 + 25 + #### Acceptance Criteria 26 + 27 + 1. WHEN the Parser receives a string containing tagged entities, THE Parser SHALL extract all entities with their type and value 28 + 2. WHEN the Parser receives a string without any tagged entities, THE Parser SHALL return an empty entity collection 29 + 3. THE Parser SHALL preserve the original string text during parsing 30 + 4. THE Parser SHALL support multiple entities within a single string 31 + 5. WHEN the Parser encounters malformed tag syntax, THE Parser SHALL skip the malformed tag and continue parsing 32 + 33 + ### Requirement 2 34 + 35 + **User Story:** As a developer, I want to define custom tag syntax, so that I can adapt the parser to different conventions 36 + 37 + #### Acceptance Criteria 38 + 39 + 1. THE Parser SHALL accept configuration for tag opening delimiter 40 + 2. THE Parser SHALL accept configuration for tag closing delimiter 41 + 3. THE Parser SHALL accept configuration for type-value separator syntax 42 + 4. WHEN no configuration is provided, THE Parser SHALL use default tag syntax 43 + 5. THE Parser SHALL validate configuration parameters before parsing 44 + 45 + ### Requirement 3 46 + 47 + **User Story:** As a developer, I want to access parsed entities by type, so that I can easily retrieve specific kinds of information from strings 48 + 49 + #### Acceptance Criteria 50 + 51 + 1. THE Parser SHALL provide a method to retrieve all entities of a specific type 52 + 2. THE Parser SHALL provide a method to retrieve all parsed entities 53 + 3. THE Parser SHALL return entities in the order they appear in the string 54 + 4. WHEN no entities of a requested type exist, THE Parser SHALL return an empty collection 55 + 56 + ### Requirement 4 57 + 58 + **User Story:** As a developer, I want to define a schema for known entity types, so that the parser can provide typed values for entities I care about 59 + 60 + #### Acceptance Criteria 61 + 62 + 1. THE Parser SHALL accept an optional schema mapping entity type names to expected data types 63 + 2. WHEN the Parser encounters a Known Entity, THE Parser SHALL parse the value according to the schema type 64 + 3. WHEN the Parser encounters an Unknown Entity, THE Parser SHALL infer the primitive type from the value 65 + 4. THE Parser SHALL support string, number, and boolean primitive types for Unknown Entities 66 + 5. THE Parser SHALL expose typed values to consumers for programmatic formatting 67 + 68 + ### Requirement 5 69 + 70 + **User Story:** As a developer, I want to apply custom formatters to entity values, so that I can control how entities are displayed in output 71 + 72 + #### Acceptance Criteria 73 + 74 + 1. THE Parser SHALL accept optional formatter functions in the schema for each entity type 75 + 2. WHEN a formatter is provided for an entity type, THE Parser SHALL apply the formatter to the parsed value 76 + 3. WHEN no formatter is provided, THE Parser SHALL convert the parsed value to string 77 + 4. THE Parser SHALL store the formatted result in the Entity formattedValue property 78 + 5. THE Parser SHALL provide a format method on ParseResult that reconstructs the message with formatted entities 79 + 80 + ### Requirement 6 81 + 82 + **User Story:** As a developer, I want the parser to have zero runtime dependencies and run directly with Node.js, so that I can use it without compilation overhead 83 + 84 + #### Acceptance Criteria 85 + 86 + 1. THE Parser SHALL be implemented using only TypeScript standard library features 87 + 2. THE Parser SHALL not require any third-party runtime dependencies 88 + 3. THE Parser SHALL be executable directly with Node.js v24 native TypeScript support 89 + 4. THE Parser SHALL not require compilation to JavaScript for execution
+121
.kiro/specs/tagged-string/tasks.md
··· 1 + # Implementation Plan 2 + 3 + - [x] 1. Set up project structure and type definitions 4 + - Create TypeScript configuration file (tsconfig.json) for Node v24 native execution 5 + - Define Entity interface with type, value, parsedValue, formattedValue, inferredType, and position properties 6 + - Define PrimitiveType and EntityDefinition types for schema with optional formatters 7 + - Define EntitySchema type for mapping entity types to primitive types or definitions with formatters 8 + - Define ParserConfig interface with delimiter, separator, and schema options 9 + - Define ParseResult interface with utility methods including format() 10 + - _Requirements: 6.1, 6.2, 6.3, 6.4_ 11 + 12 + - [x] 2. Implement ParseResult class 13 + - [x] 2.1 Create ParseResult class with constructor accepting original message and entities array 14 + - Store original message and entities as properties 15 + - _Requirements: 1.3_ 16 + 17 + - [x] 2.2 Implement getEntitiesByType method 18 + - Filter entities array by type parameter 19 + - Return filtered array in original order 20 + - Return empty array when no matches found 21 + - _Requirements: 3.1, 3.2, 3.3, 3.4_ 22 + 23 + - [x] 2.3 Implement getAllTypes method 24 + - Extract unique entity types from entities array 25 + - Return array of type strings 26 + - _Requirements: 3.2_ 27 + 28 + - [x] 2.4 Implement format method 29 + - Reconstruct original message replacing tags with formattedValue from entities 30 + - Use entity position to correctly place formatted values 31 + - Return formatted string 32 + - _Requirements: 5.5_ 33 + 34 + - [x] 3. Implement TaggedStringParser class 35 + - [x] 3.1 Create parser class with configuration support 36 + - Accept optional ParserConfig in constructor (including schema) 37 + - Set default delimiters: '[' and ']' 38 + - Set default type separator: ':' 39 + - Store schema for entity type lookup 40 + - Validate configuration (no empty delimiters, no delimiter conflicts) 41 + - _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 4.1, 5.1_ 42 + 43 + - [x] 3.2 Implement type inference and parsing helpers 44 + - Create inferType method to detect number, boolean, or string from raw value 45 + - Create parseValue method that uses schema (if available) or falls back to inference 46 + - Handle number parsing (including decimals and negatives) 47 + - Handle boolean parsing (case-insensitive true/false) 48 + - Default to string for all other values 49 + - _Requirements: 4.2, 4.3, 4.4, 4.5_ 50 + 51 + - [x] 3.3 Implement formatter application 52 + - Create applyFormatter method that checks schema for formatter function 53 + - If formatter exists, apply it to parsedValue and return result 54 + - If no formatter, convert parsedValue to string 55 + - Store result as formattedValue in entity 56 + - _Requirements: 5.1, 5.2, 5.3, 5.4_ 57 + 58 + - [x] 3.4 Implement core parsing logic in parse method 59 + - Create character-by-character scanner 60 + - Track parser state (inside/outside tag) 61 + - Accumulate tag content when inside tag boundaries 62 + - Extract entity type and raw value from tag content using separator 63 + - Call parseValue to get typed parsedValue and inferredType 64 + - Call applyFormatter to get formattedValue 65 + - Record entity position in original string 66 + - Handle malformed tags by skipping and continuing 67 + - Return ParseResult with original message and extracted entities 68 + - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 3.3, 4.2, 4.5, 5.4_ 69 + 70 + - [x] 3.5 Handle edge cases in parsing 71 + - Return empty entities array for empty input strings 72 + - Skip unclosed tags at end of string 73 + - Handle tags without type separator (treat as value with empty type) 74 + - Skip empty tags 75 + - Preserve quoted values in entity values 76 + - _Requirements: 1.2, 1.5_ 77 + 78 + - [x] 4. Write unit tests for ParseResult 79 + - Test getEntitiesByType with matching and non-matching types 80 + - Test getAllTypes with multiple and zero entities 81 + - Test entity order preservation 82 + - Test format method reconstructs message with formatted entities 83 + - Test format method with entities that have custom formatters 84 + - _Requirements: 3.1, 3.2, 3.3, 3.4, 5.5_ 85 + 86 + - [x] 5. Write unit tests for TaggedStringParser 87 + - Test single entity extraction 88 + - Test multiple entities in one message 89 + - Test messages without entities 90 + - Test custom delimiter configuration 91 + - Test configuration validation (invalid delimiters) 92 + - Test malformed tag handling (unclosed, missing separator, empty) 93 + - Test entity position tracking 94 + - Test schema-based type parsing for known entities 95 + - Test automatic type inference for unknown entities (numbers, booleans, strings) 96 + - Test mixed known and unknown entities in same message 97 + - Test formatter functions applied to entity values 98 + - Test entities without formatters default to string conversion 99 + - Test shorthand schema syntax (just type) vs full EntityDefinition with formatter 100 + - Test real-world IaC log examples from design document 101 + - _Requirements: 1.1, 1.2, 1.4, 1.5, 2.1, 2.2, 2.3, 2.4, 2.5, 4.2, 4.3, 4.4, 4.5, 5.1, 5.2, 5.3, 5.4_ 102 + 103 + - [x] 6. Create example usage file 104 + - Create examples.ts demonstrating basic usage with schema 105 + - Show schema definition for known entity types with formatters 106 + - Include example with chalk or simple string formatters (String.trim, toUpperCase, etc.) 107 + - Include IaC logging examples from design document 108 + - Show custom configuration usage 109 + - Show entity filtering by type 110 + - Demonstrate accessing parsedValue, formattedValue, and inferredType properties 111 + - Show examples of automatic type inference for unknown entities 112 + - Demonstrate format() method to get fully formatted message 113 + - _Requirements: 1.1, 2.1, 3.1, 4.2, 4.5, 5.1, 5.5_ 114 + 115 + - [x] 7. Create package.json and Node v24 configuration 116 + - Set up package.json with TypeScript as dev dependency only 117 + - Configure for Node v24 native TypeScript execution 118 + - Add script to run examples directly without compilation 119 + - Set module type and entry points 120 + - Verify zero runtime dependencies 121 + - _Requirements: 6.1, 6.2, 6.3, 6.4_
+15
.kiro/steering/product.md
··· 1 + # Product Overview 2 + 3 + Tagged String is a lightweight, zero-dependency TypeScript library for extracting structured entity information from strings using tag-based syntax (e.g., `[operation:OP-123]`). 4 + 5 + ## Core Purpose 6 + 7 + Parse tagged entities from strings while maintaining readability, enabling programmatic access to structured data embedded in text. Designed for IaC systems and other applications that need to extract typed information from string output. 8 + 9 + ## Key Features 10 + 11 + - Tag-based entity extraction with configurable delimiters 12 + - Schema-based type parsing for known entities with optional formatters 13 + - Automatic type inference (string, number, boolean) for unknown entities 14 + - Entity filtering and message reconstruction with formatted values 15 + - Zero runtime dependencies, runs natively on Node.js v24
+43
.kiro/steering/structure.md
··· 1 + # Project Structure 2 + 3 + ## Directory Layout 4 + 5 + ``` 6 + . 7 + ├── .kiro/ 8 + │ ├── specs/ # Feature specifications (requirements, design, tasks) 9 + │ └── steering/ # AI assistant guidance documents 10 + ├── src/ 11 + │ └── types.ts # Core type definitions and interfaces 12 + ├── dist/ # Compiled output (generated) 13 + ├── TODO.md # Project task list (keep updated) 14 + └── tsconfig.json # TypeScript configuration 15 + ``` 16 + 17 + ## Code Organization 18 + 19 + ### Type Definitions (`src/types.ts`) 20 + 21 + Central location for all TypeScript interfaces and types: 22 + - `PrimitiveType`: Supported primitive types (string, number, boolean) 23 + - `EntityDefinition`: Entity schema with optional formatter 24 + - `EntitySchema`: Schema mapping for entity types 25 + - `Entity`: Parsed entity structure with type, value, parsedValue, formattedValue, inferredType, position 26 + - `ParserConfig`: Parser configuration options 27 + - `ParseResult`: Parse result with utility methods 28 + 29 + ### Implementation Files (to be created) 30 + 31 + - Parser class implementation 32 + - ParseResult class implementation 33 + - Helper functions for type inference and formatting 34 + 35 + ## Conventions 36 + 37 + - All interfaces and types defined before implementation 38 + - Comprehensive JSDoc comments on public interfaces 39 + - Single-pass parsing architecture 40 + - Lenient error handling (skip malformed input, don't throw) 41 + - Position tracking for all extracted entities 42 + - Keep TODO.md updated with current tasks and progress 43 + - Add issues, bugs, and important features to TODO.md when discovered
+29
.kiro/steering/tech.md
··· 1 + # Technology Stack 2 + 3 + ## Runtime 4 + 5 + - **Node.js**: v24+ with native TypeScript support 6 + - **Dependencies**: Zero runtime dependencies (core principle) 7 + 8 + ## TS Configuration 9 + 10 + - **Target**: ES2022 11 + - **Module System**: ESNext with Node resolution 12 + - **Strict Mode**: Enabled for type safety 13 + 14 + ## Common Commands 15 + 16 + ```bash 17 + # Run tests; no flags required 18 + npm t 19 + 20 + # Run TypeScript directly (Node v24+) 21 + node src/parser.ts 22 + ``` 23 + 24 + ## Development Principles 25 + 26 + - No third-party runtime dependencies 27 + - Direct TypeScript execution without build step during development 28 + - TypeScript standard library only 29 + - Simple, maintainable implementations over complex optimizations
+177
README.md
··· 1 + # Tagged String 2 + 3 + Extract structured data from strings using tag-based syntax. Zero dependencies, runs natively on Node.js v24+. 4 + 5 + ```typescript 6 + import { TaggedStringParser } from 'tagged-string'; 7 + 8 + const parser = new TaggedStringParser(); 9 + const result = parser.parse('[operation:deploy] started with [changes:5] to [stack:prod-stack]'); 10 + 11 + console.log(result.entities); 12 + // [ 13 + // { type: 'operation', value: 'deploy', parsedValue: 'deploy', inferredType: 'string', ... }, 14 + // { type: 'changes', value: '5', parsedValue: 5, inferredType: 'number', ... }, 15 + // { type: 'stack', value: 'prod-stack', parsedValue: 'prod-stack', inferredType: 'string', ... } 16 + // ] 17 + ``` 18 + 19 + ## Installation 20 + 21 + ```bash 22 + npm install tagged-string 23 + ``` 24 + 25 + Requires Node.js v24 or later for native TypeScript support. 26 + 27 + ## Usage 28 + 29 + ### Basic Parsing 30 + 31 + The parser extracts `[type:value]` tags from strings and automatically infers types: 32 + 33 + ```typescript 34 + const parser = new TaggedStringParser(); 35 + const result = parser.parse('[count:42] items processed, [enabled:true] flag set'); 36 + 37 + result.entities.forEach(entity => { 38 + console.log(entity.type, entity.parsedValue, entity.inferredType); 39 + }); 40 + // count 42 number 41 + // enabled true boolean 42 + ``` 43 + 44 + ### Schema-Based Parsing 45 + 46 + Define a schema to enforce types and add formatters: 47 + 48 + ```typescript 49 + const parser = new TaggedStringParser({ 50 + schema: { 51 + operation: { type: 'string', format: (v) => v.toUpperCase() }, 52 + changes: { type: 'number', format: (n) => `${n} changes` }, 53 + stack: 'string', // shorthand without formatter 54 + } 55 + }); 56 + 57 + const result = parser.parse('[operation:deploy] started with [changes:5] to [stack:prod-stack]'); 58 + console.log(result.format()); 59 + // "DEPLOY started with 5 changes to prod-stack" 60 + ``` 61 + 62 + ### Filtering Entities 63 + 64 + ```typescript 65 + const result = parser.parse('[action:create] [resource:function] with [count:3] instances'); 66 + 67 + result.getEntitiesByType('action'); // [{ type: 'action', parsedValue: 'create', ... }] 68 + result.getAllTypes(); // ['action', 'resource', 'count'] 69 + ``` 70 + 71 + ### Custom Delimiters 72 + 73 + ```typescript 74 + const parser = new TaggedStringParser({ 75 + openDelimiter: '{{', 76 + closeDelimiter: '}}', 77 + typeSeparator: '=', 78 + schema: { 79 + user: { type: 'string', format: (v) => `@${v}` } 80 + } 81 + }); 82 + 83 + const result = parser.parse('User {{user=john}} performed {{count=10}} actions'); 84 + console.log(result.format()); 85 + // "User @john performed 10 actions" 86 + ``` 87 + 88 + ## API 89 + 90 + ### `TaggedStringParser` 91 + 92 + ```typescript 93 + constructor(config?: ParserConfig) 94 + ``` 95 + 96 + **Config options:** 97 + - `openDelimiter` (default: `'['`) - Opening tag delimiter 98 + - `closeDelimiter` (default: `']'`) - Closing tag delimiter 99 + - `typeSeparator` (default: `':'`) - Separator between type and value 100 + - `schema` - Entity type definitions with optional formatters 101 + 102 + ```typescript 103 + parse(message: string): ParseResult 104 + ``` 105 + 106 + Extracts all tagged entities from the message. 107 + 108 + ### `ParseResult` 109 + 110 + **Properties:** 111 + - `originalMessage: string` - The input message 112 + - `entities: Entity[]` - Extracted entities in order 113 + 114 + **Methods:** 115 + - `getEntitiesByType(type: string): Entity[]` - Filter entities by type 116 + - `getAllTypes(): string[]` - Get unique entity types 117 + - `format(): string` - Reconstruct message with formatted values 118 + 119 + ### `Entity` 120 + 121 + ```typescript 122 + interface Entity { 123 + type: string; // Entity type name 124 + value: string; // Raw string value 125 + parsedValue: string | number | boolean; // Typed value 126 + formattedValue: string; // Formatted display value 127 + inferredType: 'string' | 'number' | 'boolean'; 128 + position: number; // Start position in message 129 + endPosition: number; // End position in message 130 + } 131 + ``` 132 + 133 + ### `EntitySchema` 134 + 135 + ```typescript 136 + type EntitySchema = Record<string, PrimitiveType | EntityDefinition>; 137 + 138 + interface EntityDefinition { 139 + type: 'string' | 'number' | 'boolean'; 140 + format?: (value: any) => string; 141 + } 142 + ``` 143 + 144 + ## Type Inference 145 + 146 + Without a schema, the parser infers types automatically: 147 + 148 + - **number**: Matches `/^-?\d+(\.\d+)?$/` (integers and decimals) 149 + - **boolean**: `'true'` or `'false'` (case-insensitive) 150 + - **string**: Everything else 151 + 152 + ## Error Handling 153 + 154 + The parser is lenient by design: 155 + - Malformed tags are skipped 156 + - Unclosed tags at end of string are ignored 157 + - Empty tag content is skipped 158 + - Invalid config throws on construction 159 + 160 + ## Examples 161 + 162 + Run the included examples: 163 + 164 + ```bash 165 + node src/examples.ts 166 + ``` 167 + 168 + ## Development 169 + 170 + ```bash 171 + npm test # Run tests 172 + node src/examples.ts # Run examples 173 + ``` 174 + 175 + ## License 176 + 177 + MIT
+74
TODO.md
··· 1 + # TODO / Known Issues 2 + 3 + ## Known Limitations 4 + 5 + (No known limitations at this time) 6 + 7 + --- 8 + 9 + ## Recently Resolved 10 + 11 + ### ✅ ParseResult.format() doesn't support custom delimiters 12 + **Status:** RESOLVED 13 + **Resolved:** November 11, 2025 14 + **Solution:** Added `endPosition` field to Entity interface and updated format() method to use stored positions 15 + 16 + The `ParseResult.format()` method now correctly handles custom delimiters by storing the tag end position during parsing. The Entity interface was extended with an `endPosition` field, and the format() method uses this stored position instead of searching for hardcoded delimiters. 17 + 18 + **Implementation:** 19 + - Added `endPosition: number` to Entity interface 20 + - Modified TaggedStringParser to calculate and store tag end positions 21 + - Updated ParseResult.format() to use entity.endPosition 22 + - Added comprehensive tests for custom delimiter formatting 23 + - Updated examples to demonstrate custom delimiter usage 24 + 25 + --- 26 + 27 + ## Future Enhancements 28 + 29 + ### Nested Delimiter Support 30 + **Current behavior:** `[outer:[inner:value]]` extracts `[inner:value` as the value (stops at first closing delimiter) 31 + **Consideration:** Add support for properly parsing nested tags, either by: 32 + - Escaping inner delimiters 33 + - Counting delimiter depth 34 + - Supporting a different syntax for nested structures 35 + 36 + **Use case:** Complex structured data like `[config:{host:localhost,port:8080}]` 37 + 38 + ### Empty Type Handling 39 + **Current behavior:** `[:value]` works but creates entity with empty string type 40 + **Consideration:** Decide on explicit behavior: 41 + - Allow empty types as valid (current) 42 + - Treat as error/skip 43 + - Use a default type name like `"_default"` or `"value"` 44 + 45 + **Use case:** Quick tagging without type classification: `[:important]` or `[:TODO]` 46 + 47 + ### Multiple Separator Behavior 48 + **Current behavior:** `[type:value:extra]` splits on first `:` only, value becomes `value:extra` 49 + **Consideration:** Document this behavior explicitly or add options: 50 + - Split on first separator only (current, implicit) 51 + - Support escaped separators: `[type:value\:with\:colons]` 52 + - Allow configuration for multi-part values 53 + 54 + **Use case:** Values containing the separator character like URLs or timestamps 55 + 56 + ### NaN Handling for Invalid Numbers 57 + **Current behavior:** `[count:abc]` with `number` schema produces `NaN` as parsedValue 58 + **Consideration:** Add validation/error handling: 59 + - Throw error on invalid number 60 + - Fall back to string type 61 + - Add a `parseError` field to Entity 62 + - Provide a validation callback in schema 63 + 64 + **Use case:** Catching malformed numeric data early 65 + 66 + ### Formatter Error Handling 67 + **Current behavior:** If a custom formatter throws an error, parsing crashes 68 + **Consideration:** Make parsing more resilient: 69 + - Catch formatter errors and fall back to `String(value)` 70 + - Add error callback to config 71 + - Add `formatterError` field to Entity 72 + - Skip entities with formatter errors 73 + 74 + **Use case:** Robust parsing even with buggy custom formatters
+37
biome.json
··· 1 + { 2 + "$schema": "https://biomejs.dev/schemas/2.3.5/schema.json", 3 + "vcs": { 4 + "enabled": true, 5 + "clientKind": "git", 6 + "useIgnoreFile": true 7 + }, 8 + "files": { 9 + "ignoreUnknown": false 10 + }, 11 + "formatter": { 12 + "enabled": true, 13 + "indentStyle": "space", 14 + "indentWidth": 2 15 + }, 16 + "linter": { 17 + "enabled": true, 18 + "rules": { 19 + "recommended": true 20 + } 21 + }, 22 + "javascript": { 23 + "formatter": { 24 + "quoteStyle": "single", 25 + "trailingCommas": "all", 26 + "semicolons": "asNeeded" 27 + } 28 + }, 29 + "assist": { 30 + "enabled": true, 31 + "actions": { 32 + "source": { 33 + "organizeImports": "on" 34 + } 35 + } 36 + } 37 + }
+2
mise.toml
··· 1 + [tools] 2 + node = "24"
+215
package-lock.json
··· 1 + { 2 + "name": "tagged-string", 3 + "version": "0.0.0", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "tagged-string", 9 + "version": "0.0.0", 10 + "license": "MIT", 11 + "devDependencies": { 12 + "@biomejs/biome": "2.3.5", 13 + "@types/node": "^24.10.0", 14 + "typescript": "^5.7.2" 15 + }, 16 + "engines": { 17 + "node": ">=24.0.0" 18 + } 19 + }, 20 + "node_modules/@biomejs/biome": { 21 + "version": "2.3.5", 22 + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.5.tgz", 23 + "integrity": "sha512-HvLhNlIlBIbAV77VysRIBEwp55oM/QAjQEin74QQX9Xb259/XP/D5AGGnZMOyF1el4zcvlNYYR3AyTMUV3ILhg==", 24 + "dev": true, 25 + "license": "MIT OR Apache-2.0", 26 + "bin": { 27 + "biome": "bin/biome" 28 + }, 29 + "engines": { 30 + "node": ">=14.21.3" 31 + }, 32 + "funding": { 33 + "type": "opencollective", 34 + "url": "https://opencollective.com/biome" 35 + }, 36 + "optionalDependencies": { 37 + "@biomejs/cli-darwin-arm64": "2.3.5", 38 + "@biomejs/cli-darwin-x64": "2.3.5", 39 + "@biomejs/cli-linux-arm64": "2.3.5", 40 + "@biomejs/cli-linux-arm64-musl": "2.3.5", 41 + "@biomejs/cli-linux-x64": "2.3.5", 42 + "@biomejs/cli-linux-x64-musl": "2.3.5", 43 + "@biomejs/cli-win32-arm64": "2.3.5", 44 + "@biomejs/cli-win32-x64": "2.3.5" 45 + } 46 + }, 47 + "node_modules/@biomejs/cli-darwin-arm64": { 48 + "version": "2.3.5", 49 + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.5.tgz", 50 + "integrity": "sha512-fLdTur8cJU33HxHUUsii3GLx/TR0BsfQx8FkeqIiW33cGMtUD56fAtrh+2Fx1uhiCsVZlFh6iLKUU3pniZREQw==", 51 + "cpu": [ 52 + "arm64" 53 + ], 54 + "dev": true, 55 + "license": "MIT OR Apache-2.0", 56 + "optional": true, 57 + "os": [ 58 + "darwin" 59 + ], 60 + "engines": { 61 + "node": ">=14.21.3" 62 + } 63 + }, 64 + "node_modules/@biomejs/cli-darwin-x64": { 65 + "version": "2.3.5", 66 + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.5.tgz", 67 + "integrity": "sha512-qpT8XDqeUlzrOW8zb4k3tjhT7rmvVRumhi2657I2aGcY4B+Ft5fNwDdZGACzn8zj7/K1fdWjgwYE3i2mSZ+vOA==", 68 + "cpu": [ 69 + "x64" 70 + ], 71 + "dev": true, 72 + "license": "MIT OR Apache-2.0", 73 + "optional": true, 74 + "os": [ 75 + "darwin" 76 + ], 77 + "engines": { 78 + "node": ">=14.21.3" 79 + } 80 + }, 81 + "node_modules/@biomejs/cli-linux-arm64": { 82 + "version": "2.3.5", 83 + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.5.tgz", 84 + "integrity": "sha512-u/pybjTBPGBHB66ku4pK1gj+Dxgx7/+Z0jAriZISPX1ocTO8aHh8x8e7Kb1rB4Ms0nA/SzjtNOVJ4exVavQBCw==", 85 + "cpu": [ 86 + "arm64" 87 + ], 88 + "dev": true, 89 + "license": "MIT OR Apache-2.0", 90 + "optional": true, 91 + "os": [ 92 + "linux" 93 + ], 94 + "engines": { 95 + "node": ">=14.21.3" 96 + } 97 + }, 98 + "node_modules/@biomejs/cli-linux-arm64-musl": { 99 + "version": "2.3.5", 100 + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.5.tgz", 101 + "integrity": "sha512-eGUG7+hcLgGnMNl1KHVZUYxahYAhC462jF/wQolqu4qso2MSk32Q+QrpN7eN4jAHAg7FUMIo897muIhK4hXhqg==", 102 + "cpu": [ 103 + "arm64" 104 + ], 105 + "dev": true, 106 + "license": "MIT OR Apache-2.0", 107 + "optional": true, 108 + "os": [ 109 + "linux" 110 + ], 111 + "engines": { 112 + "node": ">=14.21.3" 113 + } 114 + }, 115 + "node_modules/@biomejs/cli-linux-x64": { 116 + "version": "2.3.5", 117 + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.5.tgz", 118 + "integrity": "sha512-XrIVi9YAW6ye0CGQ+yax0gLfx+BFOtKaNX74n+xHWla6Cl6huUmcKNO7HPx7BiKnJUzrxXY1qYlm7xMvi08X4g==", 119 + "cpu": [ 120 + "x64" 121 + ], 122 + "dev": true, 123 + "license": "MIT OR Apache-2.0", 124 + "optional": true, 125 + "os": [ 126 + "linux" 127 + ], 128 + "engines": { 129 + "node": ">=14.21.3" 130 + } 131 + }, 132 + "node_modules/@biomejs/cli-linux-x64-musl": { 133 + "version": "2.3.5", 134 + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.5.tgz", 135 + "integrity": "sha512-awVuycTPpVTH/+WDVnEEYSf6nbCBHf/4wB3lquwT7puhNg8R4XvonWNZzUsfHZrCkjkLhFH/vCZK5jHatD9FEg==", 136 + "cpu": [ 137 + "x64" 138 + ], 139 + "dev": true, 140 + "license": "MIT OR Apache-2.0", 141 + "optional": true, 142 + "os": [ 143 + "linux" 144 + ], 145 + "engines": { 146 + "node": ">=14.21.3" 147 + } 148 + }, 149 + "node_modules/@biomejs/cli-win32-arm64": { 150 + "version": "2.3.5", 151 + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.5.tgz", 152 + "integrity": "sha512-DlBiMlBZZ9eIq4H7RimDSGsYcOtfOIfZOaI5CqsWiSlbTfqbPVfWtCf92wNzx8GNMbu1s7/g3ZZESr6+GwM/SA==", 153 + "cpu": [ 154 + "arm64" 155 + ], 156 + "dev": true, 157 + "license": "MIT OR Apache-2.0", 158 + "optional": true, 159 + "os": [ 160 + "win32" 161 + ], 162 + "engines": { 163 + "node": ">=14.21.3" 164 + } 165 + }, 166 + "node_modules/@biomejs/cli-win32-x64": { 167 + "version": "2.3.5", 168 + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.5.tgz", 169 + "integrity": "sha512-nUmR8gb6yvrKhtRgzwo/gDimPwnO5a4sCydf8ZS2kHIJhEmSmk+STsusr1LHTuM//wXppBawvSQi2xFXJCdgKQ==", 170 + "cpu": [ 171 + "x64" 172 + ], 173 + "dev": true, 174 + "license": "MIT OR Apache-2.0", 175 + "optional": true, 176 + "os": [ 177 + "win32" 178 + ], 179 + "engines": { 180 + "node": ">=14.21.3" 181 + } 182 + }, 183 + "node_modules/@types/node": { 184 + "version": "24.10.0", 185 + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", 186 + "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", 187 + "dev": true, 188 + "license": "MIT", 189 + "dependencies": { 190 + "undici-types": "~7.16.0" 191 + } 192 + }, 193 + "node_modules/typescript": { 194 + "version": "5.9.3", 195 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", 196 + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", 197 + "dev": true, 198 + "license": "Apache-2.0", 199 + "bin": { 200 + "tsc": "bin/tsc", 201 + "tsserver": "bin/tsserver" 202 + }, 203 + "engines": { 204 + "node": ">=14.17" 205 + } 206 + }, 207 + "node_modules/undici-types": { 208 + "version": "7.16.0", 209 + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", 210 + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", 211 + "dev": true, 212 + "license": "MIT" 213 + } 214 + } 215 + }
+28
package.json
··· 1 + { 2 + "name": "tagged-string", 3 + "version": "0.0.0", 4 + "description": "Lightweight, zero-dependency TypeScript library for extracting structured entity information from strings using tag-based syntax", 5 + "license": "MIT", 6 + "author": "tbeseda", 7 + "type": "module", 8 + "main": "./src/TaggedStringParser.ts", 9 + "types": "./src/types.ts", 10 + "exports": { 11 + ".": "./src/TaggedStringParser.ts", 12 + "./types": "./src/types.ts" 13 + }, 14 + "scripts": { 15 + "lint": "biome check --write", 16 + "test": "node --test src/**/*.test.ts", 17 + "posttest": "npm run lint", 18 + "examples": "node --experimental-strip-types src/examples.ts" 19 + }, 20 + "engines": { 21 + "node": ">=24.0.0" 22 + }, 23 + "devDependencies": { 24 + "@biomejs/biome": "2.3.5", 25 + "@types/node": "^24.10.0", 26 + "typescript": "^5.7.2" 27 + } 28 + }
+547
src/ParseResult.test.ts
··· 1 + import assert from 'node:assert' 2 + import { describe, test } from 'node:test' 3 + import { ParseResult } from './ParseResult.ts' 4 + import type { Entity } from './types.ts' 5 + 6 + describe('ParseResult', () => { 7 + describe('getEntitiesByType', () => { 8 + test('should return entities matching the specified type', () => { 9 + const entities: Entity[] = [ 10 + { 11 + type: 'operation', 12 + value: 'OP-123', 13 + parsedValue: 'OP-123', 14 + formattedValue: 'OP-123', 15 + inferredType: 'string', 16 + position: 0, 17 + endPosition: 18, 18 + }, 19 + { 20 + type: 'count', 21 + value: '5', 22 + parsedValue: 5, 23 + formattedValue: '5', 24 + inferredType: 'number', 25 + position: 20, 26 + endPosition: 28, 27 + }, 28 + { 29 + type: 'operation', 30 + value: 'OP-456', 31 + parsedValue: 'OP-456', 32 + formattedValue: 'OP-456', 33 + inferredType: 'string', 34 + position: 30, 35 + endPosition: 48, 36 + }, 37 + ] 38 + const result = new ParseResult( 39 + '[operation:OP-123] [count:5] [operation:OP-456]', 40 + entities, 41 + ) 42 + 43 + const operations = result.getEntitiesByType('operation') 44 + 45 + assert.strictEqual(operations.length, 2) 46 + assert.strictEqual(operations[0].value, 'OP-123') 47 + assert.strictEqual(operations[1].value, 'OP-456') 48 + }) 49 + 50 + test('should return empty array for non-matching type', () => { 51 + const entities: Entity[] = [ 52 + { 53 + type: 'operation', 54 + value: 'OP-123', 55 + parsedValue: 'OP-123', 56 + formattedValue: 'OP-123', 57 + inferredType: 'string', 58 + position: 0, 59 + endPosition: 18, 60 + }, 61 + ] 62 + const result = new ParseResult('[operation:OP-123]', entities) 63 + 64 + const stacks = result.getEntitiesByType('stack') 65 + 66 + assert.strictEqual(stacks.length, 0) 67 + assert.ok(Array.isArray(stacks)) 68 + }) 69 + 70 + test('should preserve entity order', () => { 71 + const entities: Entity[] = [ 72 + { 73 + type: 'item', 74 + value: 'first', 75 + parsedValue: 'first', 76 + formattedValue: 'first', 77 + inferredType: 'string', 78 + position: 0, 79 + endPosition: 12, 80 + }, 81 + { 82 + type: 'item', 83 + value: 'second', 84 + parsedValue: 'second', 85 + formattedValue: 'second', 86 + inferredType: 'string', 87 + position: 10, 88 + endPosition: 23, 89 + }, 90 + { 91 + type: 'item', 92 + value: 'third', 93 + parsedValue: 'third', 94 + formattedValue: 'third', 95 + inferredType: 'string', 96 + position: 20, 97 + endPosition: 32, 98 + }, 99 + ] 100 + const result = new ParseResult( 101 + '[item:first] [item:second] [item:third]', 102 + entities, 103 + ) 104 + 105 + const items = result.getEntitiesByType('item') 106 + 107 + assert.strictEqual(items[0].value, 'first') 108 + assert.strictEqual(items[1].value, 'second') 109 + assert.strictEqual(items[2].value, 'third') 110 + }) 111 + }) 112 + 113 + describe('getAllTypes', () => { 114 + test('should return all unique entity types', () => { 115 + const entities: Entity[] = [ 116 + { 117 + type: 'operation', 118 + value: 'OP-123', 119 + parsedValue: 'OP-123', 120 + formattedValue: 'OP-123', 121 + inferredType: 'string', 122 + position: 0, 123 + endPosition: 18, 124 + }, 125 + { 126 + type: 'count', 127 + value: '5', 128 + parsedValue: 5, 129 + formattedValue: '5', 130 + inferredType: 'number', 131 + position: 20, 132 + endPosition: 28, 133 + }, 134 + { 135 + type: 'stack', 136 + value: 'ST-456', 137 + parsedValue: 'ST-456', 138 + formattedValue: 'ST-456', 139 + inferredType: 'string', 140 + position: 30, 141 + endPosition: 43, 142 + }, 143 + { 144 + type: 'operation', 145 + value: 'OP-789', 146 + parsedValue: 'OP-789', 147 + formattedValue: 'OP-789', 148 + inferredType: 'string', 149 + position: 45, 150 + endPosition: 63, 151 + }, 152 + ] 153 + const result = new ParseResult( 154 + '[operation:OP-123] [count:5] [stack:ST-456] [operation:OP-789]', 155 + entities, 156 + ) 157 + 158 + const types = result.getAllTypes() 159 + 160 + assert.strictEqual(types.length, 3) 161 + assert.ok(types.includes('operation')) 162 + assert.ok(types.includes('count')) 163 + assert.ok(types.includes('stack')) 164 + }) 165 + 166 + test('should return empty array when no entities exist', () => { 167 + const result = new ParseResult('No entities here', []) 168 + 169 + const types = result.getAllTypes() 170 + 171 + assert.strictEqual(types.length, 0) 172 + assert.ok(Array.isArray(types)) 173 + }) 174 + }) 175 + 176 + describe('format', () => { 177 + test('should reconstruct message with formatted entity values', () => { 178 + const entities: Entity[] = [ 179 + { 180 + type: 'operation', 181 + value: 'OP-123', 182 + parsedValue: 'OP-123', 183 + formattedValue: 'OP-123', 184 + inferredType: 'string', 185 + position: 0, 186 + endPosition: 18, 187 + }, 188 + { 189 + type: 'count', 190 + value: '5', 191 + parsedValue: 5, 192 + formattedValue: '5', 193 + inferredType: 'number', 194 + position: 32, 195 + endPosition: 41, 196 + }, 197 + ] 198 + const result = new ParseResult( 199 + '[operation:OP-123] started with [count:5]', 200 + entities, 201 + ) 202 + 203 + const formatted = result.format() 204 + 205 + assert.strictEqual(formatted, 'OP-123 started with 5') 206 + }) 207 + 208 + test('should apply custom formatters to entity values', () => { 209 + const entities: Entity[] = [ 210 + { 211 + type: 'operation', 212 + value: 'OP-123', 213 + parsedValue: 'OP-123', 214 + formattedValue: '**OP-123**', 215 + inferredType: 'string', 216 + position: 0, 217 + endPosition: 18, 218 + }, 219 + { 220 + type: 'count', 221 + value: '5', 222 + parsedValue: 5, 223 + formattedValue: '[5 items]', 224 + inferredType: 'number', 225 + position: 32, 226 + endPosition: 41, 227 + }, 228 + ] 229 + const result = new ParseResult( 230 + '[operation:OP-123] started with [count:5]', 231 + entities, 232 + ) 233 + 234 + const formatted = result.format() 235 + 236 + assert.strictEqual(formatted, '**OP-123** started with [5 items]') 237 + }) 238 + 239 + test('should handle multiple entities with different formatters', () => { 240 + const entities: Entity[] = [ 241 + { 242 + type: 'name', 243 + value: 'alice', 244 + parsedValue: 'alice', 245 + formattedValue: 'ALICE', 246 + inferredType: 'string', 247 + position: 0, 248 + endPosition: 12, 249 + }, 250 + { 251 + type: 'count', 252 + value: '5', 253 + parsedValue: 5, 254 + formattedValue: '5 items', 255 + inferredType: 'number', 256 + position: 17, 257 + endPosition: 26, 258 + }, 259 + ] 260 + const result = new ParseResult( 261 + '[name:alice] has [count:5] total', 262 + entities, 263 + ) 264 + 265 + const formatted = result.format() 266 + 267 + assert.strictEqual(formatted, 'ALICE has 5 items total') 268 + }) 269 + 270 + test('should return original message when no entities exist', () => { 271 + const result = new ParseResult('No entities here', []) 272 + 273 + const formatted = result.format() 274 + 275 + assert.strictEqual(formatted, 'No entities here') 276 + }) 277 + 278 + test('should preserve text between and around entities', () => { 279 + const entities: Entity[] = [ 280 + { 281 + type: 'a', 282 + value: '1', 283 + parsedValue: 1, 284 + formattedValue: 'ONE', 285 + inferredType: 'number', 286 + position: 7, 287 + endPosition: 12, 288 + }, 289 + { 290 + type: 'b', 291 + value: '2', 292 + parsedValue: 2, 293 + formattedValue: 'TWO', 294 + inferredType: 'number', 295 + position: 20, 296 + endPosition: 25, 297 + }, 298 + ] 299 + const result = new ParseResult('Start: [a:1] middle [b:2] end', entities) 300 + 301 + const formatted = result.format() 302 + 303 + assert.strictEqual(formatted, 'Start: ONE middle TWO end') 304 + }) 305 + }) 306 + 307 + describe('format with custom delimiters', () => { 308 + test('should format message with custom single-character delimiters', () => { 309 + const entities: Entity[] = [ 310 + { 311 + type: 'operation', 312 + value: 'OP-123', 313 + parsedValue: 'OP-123', 314 + formattedValue: 'OP-123', 315 + inferredType: 'string', 316 + position: 0, 317 + endPosition: 18, 318 + }, 319 + { 320 + type: 'count', 321 + value: '5', 322 + parsedValue: 5, 323 + formattedValue: '5', 324 + inferredType: 'number', 325 + position: 32, 326 + endPosition: 41, 327 + }, 328 + ] 329 + const result = new ParseResult( 330 + '<operation:OP-123> started with <count:5>', 331 + entities, 332 + '>', 333 + ) 334 + 335 + const formatted = result.format() 336 + 337 + assert.strictEqual(formatted, 'OP-123 started with 5') 338 + }) 339 + 340 + test('should format message with custom multi-character delimiters', () => { 341 + const entities: Entity[] = [ 342 + { 343 + type: 'user', 344 + value: 'john', 345 + parsedValue: 'john', 346 + formattedValue: '@john', 347 + inferredType: 'string', 348 + position: 5, 349 + endPosition: 18, 350 + }, 351 + { 352 + type: 'count', 353 + value: '10', 354 + parsedValue: 10, 355 + formattedValue: '10', 356 + inferredType: 'number', 357 + position: 29, 358 + endPosition: 41, 359 + }, 360 + ] 361 + const result = new ParseResult( 362 + 'User {{user:john}} performed {{count:10}} actions', 363 + entities, 364 + '}}', 365 + ) 366 + 367 + const formatted = result.format() 368 + 369 + assert.strictEqual(formatted, 'User @john performed 10 actions') 370 + }) 371 + 372 + test('should format message with custom type separator', () => { 373 + const entities: Entity[] = [ 374 + { 375 + type: 'operation', 376 + value: 'OP-123', 377 + parsedValue: 'OP-123', 378 + formattedValue: 'OP-123', 379 + inferredType: 'string', 380 + position: 0, 381 + endPosition: 18, 382 + }, 383 + ] 384 + const result = new ParseResult('[operation=OP-123]', entities, ']') 385 + 386 + const formatted = result.format() 387 + 388 + assert.strictEqual(formatted, 'OP-123') 389 + }) 390 + 391 + test('should format message with all custom delimiters combined', () => { 392 + const entities: Entity[] = [ 393 + { 394 + type: 'operation', 395 + value: 'OP-123', 396 + parsedValue: 'OP-123', 397 + formattedValue: '**OP-123**', 398 + inferredType: 'string', 399 + position: 0, 400 + endPosition: 20, 401 + }, 402 + { 403 + type: 'count', 404 + value: '5', 405 + parsedValue: 5, 406 + formattedValue: '[5 items]', 407 + inferredType: 'number', 408 + position: 34, 409 + endPosition: 46, 410 + }, 411 + ] 412 + const result = new ParseResult( 413 + '<<operation|OP-123>> started with <<count|5>>', 414 + entities, 415 + '>>', 416 + ) 417 + 418 + const formatted = result.format() 419 + 420 + assert.strictEqual(formatted, '**OP-123** started with [5 items]') 421 + }) 422 + 423 + test('should maintain backward compatibility with default delimiters', () => { 424 + const entities: Entity[] = [ 425 + { 426 + type: 'operation', 427 + value: 'OP-123', 428 + parsedValue: 'OP-123', 429 + formattedValue: 'OP-123', 430 + inferredType: 'string', 431 + position: 0, 432 + endPosition: 18, 433 + }, 434 + ] 435 + const result = new ParseResult('[operation:OP-123]', entities) 436 + 437 + const formatted = result.format() 438 + 439 + assert.strictEqual(formatted, 'OP-123') 440 + }) 441 + 442 + test('should correctly apply formatters with custom delimiters', () => { 443 + const entities: Entity[] = [ 444 + { 445 + type: 'user', 446 + value: 'alice', 447 + parsedValue: 'alice', 448 + formattedValue: 'ALICE', 449 + inferredType: 'string', 450 + position: 0, 451 + endPosition: 12, 452 + }, 453 + { 454 + type: 'count', 455 + value: '42', 456 + parsedValue: 42, 457 + formattedValue: '42 items', 458 + inferredType: 'number', 459 + position: 17, 460 + endPosition: 27, 461 + }, 462 + ] 463 + const result = new ParseResult( 464 + '{user:alice} has {count:42}', 465 + entities, 466 + '}', 467 + ) 468 + 469 + const formatted = result.format() 470 + 471 + assert.strictEqual(formatted, 'ALICE has 42 items') 472 + }) 473 + 474 + test('should format multiple entities using custom delimiters', () => { 475 + const entities: Entity[] = [ 476 + { 477 + type: 'a', 478 + value: '1', 479 + parsedValue: 1, 480 + formattedValue: 'ONE', 481 + inferredType: 'number', 482 + position: 7, 483 + endPosition: 12, 484 + }, 485 + { 486 + type: 'b', 487 + value: '2', 488 + parsedValue: 2, 489 + formattedValue: 'TWO', 490 + inferredType: 'number', 491 + position: 20, 492 + endPosition: 25, 493 + }, 494 + { 495 + type: 'c', 496 + value: '3', 497 + parsedValue: 3, 498 + formattedValue: 'THREE', 499 + inferredType: 'number', 500 + position: 30, 501 + endPosition: 35, 502 + }, 503 + ] 504 + const result = new ParseResult( 505 + 'Start: <a:1> middle <b:2> and <c:3> end', 506 + entities, 507 + '>', 508 + ) 509 + 510 + const formatted = result.format() 511 + 512 + assert.strictEqual(formatted, 'Start: ONE middle TWO and THREE end') 513 + }) 514 + 515 + test('should handle real-world example from TODO.md with curly brace delimiters', () => { 516 + const entities: Entity[] = [ 517 + { 518 + type: 'user', 519 + value: 'john', 520 + parsedValue: 'john', 521 + formattedValue: '@john', 522 + inferredType: 'string', 523 + position: 5, 524 + endPosition: 18, 525 + }, 526 + { 527 + type: 'count', 528 + value: '10', 529 + parsedValue: 10, 530 + formattedValue: '10', 531 + inferredType: 'number', 532 + position: 29, 533 + endPosition: 41, 534 + }, 535 + ] 536 + const result = new ParseResult( 537 + 'User {{user=john}} performed {{count=10}} actions', 538 + entities, 539 + '}}', 540 + ) 541 + 542 + const formatted = result.format() 543 + 544 + assert.strictEqual(formatted, 'User @john performed 10 actions') 545 + }) 546 + }) 547 + })
+93
src/ParseResult.ts
··· 1 + import type { Entity, ParseResult as IParseResult } from './types.ts' 2 + 3 + /** 4 + * Implementation of ParseResult interface 5 + * Holds the original message and extracted entities with utility methods 6 + */ 7 + export class ParseResult implements IParseResult { 8 + public readonly originalMessage: string 9 + public readonly entities: Entity[] 10 + private readonly closeDelimiter?: string 11 + 12 + constructor( 13 + originalMessage: string, 14 + entities: Entity[], 15 + closeDelimiter?: string, 16 + ) { 17 + this.originalMessage = originalMessage 18 + this.entities = entities 19 + this.closeDelimiter = closeDelimiter 20 + } 21 + 22 + /** 23 + * Get all entities of a specific type 24 + * @param type - The entity type to filter by 25 + * @returns Array of entities matching the type, in original order 26 + */ 27 + getEntitiesByType(type: string): Entity[] { 28 + return this.entities.filter((entity) => entity.type === type) 29 + } 30 + 31 + /** 32 + * Get all unique entity types found in the message 33 + * @returns Array of unique type strings 34 + */ 35 + getAllTypes(): string[] { 36 + const types = new Set<string>() 37 + for (const entity of this.entities) { 38 + types.add(entity.type) 39 + } 40 + return Array.from(types) 41 + } 42 + 43 + /** 44 + * Reconstruct the message with formatted entity values 45 + * Replaces tags with their formattedValue from entities 46 + * @returns Formatted string with all entities replaced 47 + */ 48 + format(): string { 49 + if (this.entities.length === 0) { 50 + return this.originalMessage 51 + } 52 + 53 + // Sort entities by position to process them in order 54 + const sortedEntities = [...this.entities].sort( 55 + (a, b) => a.position - b.position, 56 + ) 57 + 58 + let result = '' 59 + let lastIndex = 0 60 + 61 + for (const entity of sortedEntities) { 62 + // Add text before this entity 63 + result += this.originalMessage.substring(lastIndex, entity.position) 64 + 65 + // Add the formatted value instead of the original tag 66 + result += entity.formattedValue 67 + 68 + // Use stored endPosition if available, otherwise fall back to searching 69 + let tagEnd: number 70 + if (entity.endPosition !== undefined) { 71 + tagEnd = entity.endPosition 72 + } else { 73 + // Fallback: search for closing delimiter 74 + const delimiter = this.closeDelimiter ?? ']' 75 + const closingDelimiterIndex = this.originalMessage.indexOf( 76 + delimiter, 77 + entity.position, 78 + ) 79 + tagEnd = 80 + closingDelimiterIndex !== -1 81 + ? closingDelimiterIndex + delimiter.length 82 + : entity.position 83 + } 84 + 85 + lastIndex = tagEnd 86 + } 87 + 88 + // Add remaining text after the last entity 89 + result += this.originalMessage.substring(lastIndex) 90 + 91 + return result 92 + } 93 + }
+593
src/TaggedStringParser.test.ts
··· 1 + import assert from 'node:assert' 2 + import { describe, test } from 'node:test' 3 + import { TaggedStringParser } from './TaggedStringParser.ts' 4 + import type { EntitySchema } from './types.ts' 5 + 6 + describe('TaggedStringParser', () => { 7 + describe('basic parsing', () => { 8 + test('should extract single entity', () => { 9 + const parser = new TaggedStringParser() 10 + const result = parser.parse('[operation:OP-123]') 11 + 12 + assert.strictEqual(result.entities.length, 1) 13 + assert.strictEqual(result.entities[0].type, 'operation') 14 + assert.strictEqual(result.entities[0].value, 'OP-123') 15 + assert.strictEqual(result.entities[0].position, 0) 16 + }) 17 + 18 + test('should extract multiple entities in one message', () => { 19 + const parser = new TaggedStringParser() 20 + const result = parser.parse( 21 + '[operation:OP-123] started with [changes:5] to [stack:ST-456]', 22 + ) 23 + 24 + assert.strictEqual(result.entities.length, 3) 25 + assert.strictEqual(result.entities[0].type, 'operation') 26 + assert.strictEqual(result.entities[0].value, 'OP-123') 27 + assert.strictEqual(result.entities[1].type, 'changes') 28 + assert.strictEqual(result.entities[1].value, '5') 29 + assert.strictEqual(result.entities[2].type, 'stack') 30 + assert.strictEqual(result.entities[2].value, 'ST-456') 31 + }) 32 + 33 + test('should handle messages without entities', () => { 34 + const parser = new TaggedStringParser() 35 + const result = parser.parse('This is a plain log message') 36 + 37 + assert.strictEqual(result.entities.length, 0) 38 + assert.strictEqual(result.originalMessage, 'This is a plain log message') 39 + }) 40 + 41 + test('should handle empty string', () => { 42 + const parser = new TaggedStringParser() 43 + const result = parser.parse('') 44 + 45 + assert.strictEqual(result.entities.length, 0) 46 + assert.strictEqual(result.originalMessage, '') 47 + }) 48 + }) 49 + 50 + describe('custom delimiter configuration', () => { 51 + test('should use custom delimiters', () => { 52 + const parser = new TaggedStringParser({ 53 + openDelimiter: '{', 54 + closeDelimiter: '}', 55 + }) 56 + const result = parser.parse('{operation:OP-123} started') 57 + 58 + assert.strictEqual(result.entities.length, 1) 59 + assert.strictEqual(result.entities[0].type, 'operation') 60 + assert.strictEqual(result.entities[0].value, 'OP-123') 61 + }) 62 + 63 + test('should use custom type separator', () => { 64 + const parser = new TaggedStringParser({ 65 + typeSeparator: '=', 66 + }) 67 + const result = parser.parse('[operation=OP-123]') 68 + 69 + assert.strictEqual(result.entities.length, 1) 70 + assert.strictEqual(result.entities[0].type, 'operation') 71 + assert.strictEqual(result.entities[0].value, 'OP-123') 72 + }) 73 + 74 + test('should use multiple custom configurations together', () => { 75 + const parser = new TaggedStringParser({ 76 + openDelimiter: '<', 77 + closeDelimiter: '>', 78 + typeSeparator: '|', 79 + }) 80 + const result = parser.parse('<operation|OP-123> started') 81 + 82 + assert.strictEqual(result.entities.length, 1) 83 + assert.strictEqual(result.entities[0].type, 'operation') 84 + assert.strictEqual(result.entities[0].value, 'OP-123') 85 + }) 86 + }) 87 + 88 + describe('configuration validation', () => { 89 + test('should throw error for empty open delimiter', () => { 90 + assert.throws( 91 + () => new TaggedStringParser({ openDelimiter: '' }), 92 + /Open delimiter cannot be empty/, 93 + ) 94 + }) 95 + 96 + test('should throw error for empty close delimiter', () => { 97 + assert.throws( 98 + () => new TaggedStringParser({ closeDelimiter: '' }), 99 + /Close delimiter cannot be empty/, 100 + ) 101 + }) 102 + 103 + test('should throw error when delimiters are the same', () => { 104 + assert.throws( 105 + () => 106 + new TaggedStringParser({ openDelimiter: '|', closeDelimiter: '|' }), 107 + /Open and close delimiters cannot be the same/, 108 + ) 109 + }) 110 + }) 111 + 112 + describe('malformed tag handling', () => { 113 + test('should skip unclosed tag at end of string', () => { 114 + const parser = new TaggedStringParser() 115 + const result = parser.parse('[operation:OP-123] started [incomplete') 116 + 117 + assert.strictEqual(result.entities.length, 1) 118 + assert.strictEqual(result.entities[0].type, 'operation') 119 + assert.strictEqual(result.entities[0].value, 'OP-123') 120 + }) 121 + 122 + test('should handle tag without type separator', () => { 123 + const parser = new TaggedStringParser() 124 + const result = parser.parse('[justvalue]') 125 + 126 + assert.strictEqual(result.entities.length, 1) 127 + assert.strictEqual(result.entities[0].type, '') 128 + assert.strictEqual(result.entities[0].value, 'justvalue') 129 + }) 130 + 131 + test('should skip empty tags', () => { 132 + const parser = new TaggedStringParser() 133 + const result = parser.parse('[operation:OP-123] [] [stack:ST-456]') 134 + 135 + assert.strictEqual(result.entities.length, 2) 136 + assert.strictEqual(result.entities[0].type, 'operation') 137 + assert.strictEqual(result.entities[1].type, 'stack') 138 + }) 139 + 140 + test('should skip tags with only whitespace', () => { 141 + const parser = new TaggedStringParser() 142 + const result = parser.parse('[operation:OP-123] [ ] [stack:ST-456]') 143 + 144 + assert.strictEqual(result.entities.length, 2) 145 + assert.strictEqual(result.entities[0].type, 'operation') 146 + assert.strictEqual(result.entities[1].type, 'stack') 147 + }) 148 + }) 149 + 150 + describe('entity position tracking', () => { 151 + test('should track position of single entity', () => { 152 + const parser = new TaggedStringParser() 153 + const result = parser.parse('[operation:OP-123]') 154 + 155 + assert.strictEqual(result.entities[0].position, 0) 156 + }) 157 + 158 + test('should track positions of multiple entities', () => { 159 + const parser = new TaggedStringParser() 160 + const result = parser.parse('[operation:OP-123] started with [changes:5]') 161 + 162 + assert.strictEqual(result.entities[0].position, 0) 163 + assert.strictEqual(result.entities[1].position, 32) 164 + }) 165 + 166 + test('should track position with text before entity', () => { 167 + const parser = new TaggedStringParser() 168 + const result = parser.parse('Starting [operation:OP-123] now') 169 + 170 + assert.strictEqual(result.entities[0].position, 9) 171 + }) 172 + }) 173 + 174 + describe('schema-based type parsing', () => { 175 + test('should parse known entity using schema type', () => { 176 + const schema: EntitySchema = { 177 + count: 'number', 178 + enabled: 'boolean', 179 + name: 'string', 180 + } 181 + const parser = new TaggedStringParser({ schema }) 182 + const result = parser.parse('[count:42] [enabled:true] [name:test]') 183 + 184 + assert.strictEqual(result.entities[0].parsedValue, 42) 185 + assert.strictEqual(result.entities[0].inferredType, 'number') 186 + assert.strictEqual(result.entities[1].parsedValue, true) 187 + assert.strictEqual(result.entities[1].inferredType, 'boolean') 188 + assert.strictEqual(result.entities[2].parsedValue, 'test') 189 + assert.strictEqual(result.entities[2].inferredType, 'string') 190 + }) 191 + 192 + test('should parse number from schema even if value looks like string', () => { 193 + const schema: EntitySchema = { 194 + id: 'number', 195 + } 196 + const parser = new TaggedStringParser({ schema }) 197 + const result = parser.parse('[id:123]') 198 + 199 + assert.strictEqual(result.entities[0].parsedValue, 123) 200 + assert.strictEqual(typeof result.entities[0].parsedValue, 'number') 201 + }) 202 + 203 + test('should parse boolean from schema case-insensitively', () => { 204 + const schema: EntitySchema = { 205 + flag: 'boolean', 206 + } 207 + const parser = new TaggedStringParser({ schema }) 208 + const result = parser.parse('[flag:TRUE] [flag:False]') 209 + 210 + assert.strictEqual(result.entities[0].parsedValue, true) 211 + assert.strictEqual(result.entities[1].parsedValue, false) 212 + }) 213 + }) 214 + 215 + describe('automatic type inference', () => { 216 + test('should infer number type for numeric values', () => { 217 + const parser = new TaggedStringParser() 218 + const result = parser.parse('[count:42] [price:19.99] [temp:-5]') 219 + 220 + assert.strictEqual(result.entities[0].parsedValue, 42) 221 + assert.strictEqual(result.entities[0].inferredType, 'number') 222 + assert.strictEqual(result.entities[1].parsedValue, 19.99) 223 + assert.strictEqual(result.entities[1].inferredType, 'number') 224 + assert.strictEqual(result.entities[2].parsedValue, -5) 225 + assert.strictEqual(result.entities[2].inferredType, 'number') 226 + }) 227 + 228 + test('should infer boolean type for true/false values', () => { 229 + const parser = new TaggedStringParser() 230 + const result = parser.parse( 231 + '[enabled:true] [disabled:false] [active:TRUE]', 232 + ) 233 + 234 + assert.strictEqual(result.entities[0].parsedValue, true) 235 + assert.strictEqual(result.entities[0].inferredType, 'boolean') 236 + assert.strictEqual(result.entities[1].parsedValue, false) 237 + assert.strictEqual(result.entities[1].inferredType, 'boolean') 238 + assert.strictEqual(result.entities[2].parsedValue, true) 239 + assert.strictEqual(result.entities[2].inferredType, 'boolean') 240 + }) 241 + 242 + test('should infer string type for other values', () => { 243 + const parser = new TaggedStringParser() 244 + const result = parser.parse( 245 + '[name:test] [id:abc123] [message:hello world]', 246 + ) 247 + 248 + assert.strictEqual(result.entities[0].parsedValue, 'test') 249 + assert.strictEqual(result.entities[0].inferredType, 'string') 250 + assert.strictEqual(result.entities[1].parsedValue, 'abc123') 251 + assert.strictEqual(result.entities[1].inferredType, 'string') 252 + assert.strictEqual(result.entities[2].parsedValue, 'hello world') 253 + assert.strictEqual(result.entities[2].inferredType, 'string') 254 + }) 255 + }) 256 + 257 + describe('mixed known and unknown entities', () => { 258 + test('should handle both schema-defined and inferred entities', () => { 259 + const schema: EntitySchema = { 260 + operation: 'string', 261 + changes: 'number', 262 + } 263 + const parser = new TaggedStringParser({ schema }) 264 + const result = parser.parse( 265 + '[operation:OP-123] with [changes:5] and [unknown:42]', 266 + ) 267 + 268 + // Schema-defined entities 269 + assert.strictEqual(result.entities[0].type, 'operation') 270 + assert.strictEqual(result.entities[0].parsedValue, 'OP-123') 271 + assert.strictEqual(result.entities[0].inferredType, 'string') 272 + 273 + assert.strictEqual(result.entities[1].type, 'changes') 274 + assert.strictEqual(result.entities[1].parsedValue, 5) 275 + assert.strictEqual(result.entities[1].inferredType, 'number') 276 + 277 + // Unknown entity with inference 278 + assert.strictEqual(result.entities[2].type, 'unknown') 279 + assert.strictEqual(result.entities[2].parsedValue, 42) 280 + assert.strictEqual(result.entities[2].inferredType, 'number') 281 + }) 282 + }) 283 + 284 + describe('formatter functions', () => { 285 + test('should apply formatter to entity values', () => { 286 + const schema: EntitySchema = { 287 + operation: { 288 + type: 'string', 289 + format: (val) => `**${val}**`, 290 + }, 291 + count: { 292 + type: 'number', 293 + format: (val) => `[${val} items]`, 294 + }, 295 + } 296 + const parser = new TaggedStringParser({ schema }) 297 + const result = parser.parse('[operation:OP-123] has [count:5]') 298 + 299 + assert.strictEqual(result.entities[0].formattedValue, '**OP-123**') 300 + assert.strictEqual(result.entities[1].formattedValue, '[5 items]') 301 + }) 302 + 303 + test('should apply formatter with uppercase transformation', () => { 304 + const schema: EntitySchema = { 305 + name: { 306 + type: 'string', 307 + format: (val) => val.toUpperCase(), 308 + }, 309 + } 310 + const parser = new TaggedStringParser({ schema }) 311 + const result = parser.parse('[name:alice]') 312 + 313 + assert.strictEqual(result.entities[0].formattedValue, 'ALICE') 314 + }) 315 + }) 316 + 317 + describe('entities without formatters', () => { 318 + test('should default to string conversion when no formatter provided', () => { 319 + const schema: EntitySchema = { 320 + count: 'number', 321 + enabled: 'boolean', 322 + } 323 + const parser = new TaggedStringParser({ schema }) 324 + const result = parser.parse('[count:42] [enabled:true]') 325 + 326 + assert.strictEqual(result.entities[0].formattedValue, '42') 327 + assert.strictEqual(result.entities[1].formattedValue, 'true') 328 + }) 329 + 330 + test('should convert unknown entities to string', () => { 331 + const parser = new TaggedStringParser() 332 + const result = parser.parse('[count:42] [enabled:true] [name:test]') 333 + 334 + assert.strictEqual(result.entities[0].formattedValue, '42') 335 + assert.strictEqual(result.entities[1].formattedValue, 'true') 336 + assert.strictEqual(result.entities[2].formattedValue, 'test') 337 + }) 338 + }) 339 + 340 + describe('shorthand vs full EntityDefinition', () => { 341 + test('should support shorthand schema syntax', () => { 342 + const schema: EntitySchema = { 343 + count: 'number', 344 + name: 'string', 345 + } 346 + const parser = new TaggedStringParser({ schema }) 347 + const result = parser.parse('[count:42] [name:test]') 348 + 349 + assert.strictEqual(result.entities[0].parsedValue, 42) 350 + assert.strictEqual(result.entities[0].inferredType, 'number') 351 + assert.strictEqual(result.entities[1].parsedValue, 'test') 352 + assert.strictEqual(result.entities[1].inferredType, 'string') 353 + }) 354 + 355 + test('should support full EntityDefinition with formatter', () => { 356 + const schema: EntitySchema = { 357 + count: { 358 + type: 'number', 359 + format: (val) => `Count: ${val}`, 360 + }, 361 + } 362 + const parser = new TaggedStringParser({ schema }) 363 + const result = parser.parse('[count:42]') 364 + 365 + assert.strictEqual(result.entities[0].parsedValue, 42) 366 + assert.strictEqual(result.entities[0].formattedValue, 'Count: 42') 367 + }) 368 + 369 + test('should mix shorthand and full definitions in same schema', () => { 370 + const schema: EntitySchema = { 371 + count: 'number', 372 + name: { 373 + type: 'string', 374 + format: (val) => val.toUpperCase(), 375 + }, 376 + } 377 + const parser = new TaggedStringParser({ schema }) 378 + const result = parser.parse('[count:42] [name:alice]') 379 + 380 + assert.strictEqual(result.entities[0].parsedValue, 42) 381 + assert.strictEqual(result.entities[0].formattedValue, '42') 382 + assert.strictEqual(result.entities[1].parsedValue, 'alice') 383 + assert.strictEqual(result.entities[1].formattedValue, 'ALICE') 384 + }) 385 + }) 386 + 387 + describe('endPosition calculation', () => { 388 + test('should calculate endPosition with single-character delimiters (default)', () => { 389 + const parser = new TaggedStringParser() 390 + const result = parser.parse('[operation:OP-123]') 391 + 392 + assert.strictEqual(result.entities[0].position, 0) 393 + assert.strictEqual(result.entities[0].endPosition, 18) 394 + }) 395 + 396 + test('should calculate endPosition with multi-character delimiters', () => { 397 + const parser = new TaggedStringParser({ 398 + openDelimiter: '{{', 399 + closeDelimiter: '}}', 400 + }) 401 + const result = parser.parse('{{operation:OP-123}}') 402 + 403 + assert.strictEqual(result.entities[0].position, 0) 404 + assert.strictEqual(result.entities[0].endPosition, 20) 405 + }) 406 + 407 + test('should calculate endPosition for multiple entities in one message', () => { 408 + const parser = new TaggedStringParser() 409 + const result = parser.parse( 410 + '[operation:OP-123] started with [changes:5] to [stack:ST-456]', 411 + ) 412 + 413 + assert.strictEqual(result.entities.length, 3) 414 + 415 + // First entity 416 + assert.strictEqual(result.entities[0].position, 0) 417 + assert.strictEqual(result.entities[0].endPosition, 18) 418 + 419 + // Second entity 420 + assert.strictEqual(result.entities[1].position, 32) 421 + assert.strictEqual(result.entities[1].endPosition, 43) 422 + 423 + // Third entity 424 + assert.strictEqual(result.entities[2].position, 47) 425 + assert.strictEqual(result.entities[2].endPosition, 61) 426 + }) 427 + 428 + test('should calculate endPosition for entity at start of message', () => { 429 + const parser = new TaggedStringParser() 430 + const result = parser.parse('[operation:OP-123] started') 431 + 432 + assert.strictEqual(result.entities[0].position, 0) 433 + assert.strictEqual(result.entities[0].endPosition, 18) 434 + }) 435 + 436 + test('should calculate endPosition for entity in middle of message', () => { 437 + const parser = new TaggedStringParser() 438 + const result = parser.parse('Starting [operation:OP-123] now') 439 + 440 + assert.strictEqual(result.entities[0].position, 9) 441 + assert.strictEqual(result.entities[0].endPosition, 27) 442 + }) 443 + 444 + test('should calculate endPosition for entity at end of message', () => { 445 + const parser = new TaggedStringParser() 446 + const result = parser.parse('Completed [operation:OP-123]') 447 + 448 + assert.strictEqual(result.entities[0].position, 10) 449 + assert.strictEqual(result.entities[0].endPosition, 28) 450 + }) 451 + 452 + test('should calculate endPosition correctly with custom single-character delimiters', () => { 453 + const parser = new TaggedStringParser({ 454 + openDelimiter: '<', 455 + closeDelimiter: '>', 456 + }) 457 + const result = parser.parse('<operation:OP-123>') 458 + 459 + assert.strictEqual(result.entities[0].position, 0) 460 + assert.strictEqual(result.entities[0].endPosition, 18) 461 + }) 462 + 463 + test('should calculate endPosition correctly with longer multi-character delimiters', () => { 464 + const parser = new TaggedStringParser({ 465 + openDelimiter: '<<<', 466 + closeDelimiter: '>>>', 467 + }) 468 + const result = parser.parse('<<<operation:OP-123>>>') 469 + 470 + assert.strictEqual(result.entities[0].position, 0) 471 + assert.strictEqual(result.entities[0].endPosition, 22) 472 + }) 473 + 474 + test('should calculate endPosition for multiple entities with custom delimiters', () => { 475 + const parser = new TaggedStringParser({ 476 + openDelimiter: '{{', 477 + closeDelimiter: '}}', 478 + }) 479 + const result = parser.parse( 480 + 'User {{user:john}} performed {{count:10}} actions', 481 + ) 482 + 483 + assert.strictEqual(result.entities.length, 2) 484 + 485 + // First entity 486 + assert.strictEqual(result.entities[0].position, 5) 487 + assert.strictEqual(result.entities[0].endPosition, 18) 488 + 489 + // Second entity 490 + assert.strictEqual(result.entities[1].position, 29) 491 + assert.strictEqual(result.entities[1].endPosition, 41) 492 + }) 493 + }) 494 + 495 + describe('real-world IaC log examples', () => { 496 + test('should parse operation lifecycle logs', () => { 497 + const schema: EntitySchema = { 498 + operation: 'string', 499 + changes: 'number', 500 + stack: 'string', 501 + } 502 + const parser = new TaggedStringParser({ schema }) 503 + 504 + const result1 = parser.parse( 505 + '[operation:OP-123] started with [changes:5] to [stack:ST-456]', 506 + ) 507 + assert.strictEqual(result1.entities.length, 3) 508 + assert.strictEqual(result1.entities[0].parsedValue, 'OP-123') 509 + assert.strictEqual(result1.entities[1].parsedValue, 5) 510 + assert.strictEqual(result1.entities[2].parsedValue, 'ST-456') 511 + 512 + const result2 = parser.parse( 513 + '[operation:OP-123] completed [changes:5] to [stack:ST-456]', 514 + ) 515 + assert.strictEqual(result2.entities.length, 3) 516 + }) 517 + 518 + test('should parse planning logs', () => { 519 + const schema: EntitySchema = { 520 + blueprint: 'string', 521 + stack: 'string', 522 + create: 'number', 523 + update: 'number', 524 + destroy: 'number', 525 + } 526 + const parser = new TaggedStringParser({ schema }) 527 + 528 + const result = parser.parse( 529 + '[blueprint:BP-123] plan complete with [create:2] [update:3] [destroy:1] for [stack:ST-456]', 530 + ) 531 + 532 + assert.strictEqual(result.entities.length, 5) 533 + assert.strictEqual(result.entities[0].parsedValue, 'BP-123') 534 + assert.strictEqual(result.entities[1].parsedValue, 2) 535 + assert.strictEqual(result.entities[2].parsedValue, 3) 536 + assert.strictEqual(result.entities[3].parsedValue, 1) 537 + assert.strictEqual(result.entities[4].parsedValue, 'ST-456') 538 + }) 539 + 540 + test('should parse resource command logs', () => { 541 + const schema: EntitySchema = { 542 + action: 'string', 543 + resource: 'string', 544 + resourceName: 'string', 545 + type: 'string', 546 + externalId: 'string', 547 + } 548 + const parser = new TaggedStringParser({ schema }) 549 + 550 + const result1 = parser.parse( 551 + '[action:create] executing for [resource:RS-123] [resourceName:"my-function"] [type:function]', 552 + ) 553 + assert.strictEqual(result1.entities.length, 4) 554 + assert.strictEqual(result1.entities[0].parsedValue, 'create') 555 + assert.strictEqual(result1.entities[2].parsedValue, '"my-function"') 556 + 557 + const result2 = parser.parse( 558 + '[action:create] completed for [resource:RS-123] [externalId:EXT-789]', 559 + ) 560 + assert.strictEqual(result2.entities.length, 3) 561 + }) 562 + 563 + test('should preserve quoted values in entity values', () => { 564 + const parser = new TaggedStringParser() 565 + const result = parser.parse( 566 + '[resourceName:"my-function"] [error:"Error message"]', 567 + ) 568 + 569 + assert.strictEqual(result.entities[0].value, '"my-function"') 570 + assert.strictEqual(result.entities[1].value, '"Error message"') 571 + }) 572 + 573 + test('should parse resource type-specific logs', () => { 574 + const schema: EntitySchema = { 575 + resourceType: 'string', 576 + resourceName: 'string', 577 + } 578 + const parser = new TaggedStringParser({ schema }) 579 + 580 + const result1 = parser.parse( 581 + '[resourceType:function] creating [resourceName:"my-function"]', 582 + ) 583 + assert.strictEqual(result1.entities[0].parsedValue, 'function') 584 + assert.strictEqual(result1.entities[1].parsedValue, '"my-function"') 585 + 586 + const result2 = parser.parse( 587 + '[resourceType:database] updating [resourceName:"user-db"]', 588 + ) 589 + assert.strictEqual(result2.entities[0].parsedValue, 'database') 590 + assert.strictEqual(result2.entities[1].parsedValue, '"user-db"') 591 + }) 592 + }) 593 + })
+232
src/TaggedStringParser.ts
··· 1 + import { ParseResult } from './ParseResult.ts' 2 + import type { 3 + Entity, 4 + EntitySchema, 5 + ParserConfig, 6 + PrimitiveType, 7 + } from './types.ts' 8 + 9 + /** 10 + * TaggedStringParser extracts tagged entities from strings 11 + * Supports configurable delimiters, schema-based type parsing, and automatic type inference 12 + */ 13 + export class TaggedStringParser { 14 + private readonly openDelimiter: string 15 + private readonly closeDelimiter: string 16 + private readonly typeSeparator: string 17 + private readonly schema?: EntitySchema 18 + 19 + /** 20 + * Create a new TaggedStringParser with optional configuration 21 + * @param config - Parser configuration options 22 + * @throws Error if configuration is invalid 23 + */ 24 + constructor(config?: ParserConfig) { 25 + // Set defaults 26 + this.openDelimiter = config?.openDelimiter ?? '[' 27 + this.closeDelimiter = config?.closeDelimiter ?? ']' 28 + this.typeSeparator = config?.typeSeparator ?? ':' 29 + this.schema = config?.schema 30 + 31 + // Validate configuration 32 + this.validateConfig() 33 + } 34 + 35 + /** 36 + * Validate parser configuration 37 + * @throws Error if configuration is invalid 38 + */ 39 + private validateConfig(): void { 40 + if (this.openDelimiter === '') { 41 + throw new Error('Open delimiter cannot be empty') 42 + } 43 + if (this.closeDelimiter === '') { 44 + throw new Error('Close delimiter cannot be empty') 45 + } 46 + if (this.openDelimiter === this.closeDelimiter) { 47 + throw new Error('Open and close delimiters cannot be the same') 48 + } 49 + } 50 + 51 + /** 52 + * Parse a string and extract all tagged entities 53 + * @param message - The string to parse 54 + * @returns ParseResult containing original message and extracted entities 55 + */ 56 + parse(message: string): ParseResult { 57 + if (message === '') { 58 + return new ParseResult(message, []) 59 + } 60 + 61 + // Escape special regex characters in delimiters 62 + const escapeRegex = (str: string) => 63 + str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') 64 + const openEscaped = escapeRegex(this.openDelimiter) 65 + const closeEscaped = escapeRegex(this.closeDelimiter) 66 + 67 + // Build regex to match tags: openDelimiter + content + closeDelimiter 68 + const tagRegex = new RegExp( 69 + `${openEscaped}([^${closeEscaped}]+?)${closeEscaped}`, 70 + 'g', 71 + ) 72 + 73 + const entities: Entity[] = [] 74 + 75 + for (const match of message.matchAll(tagRegex)) { 76 + const tagContent = match[1].trim() 77 + 78 + if (tagContent !== '' && match.index !== undefined) { 79 + const entity = this.processTag( 80 + tagContent, 81 + match.index, 82 + match.index + match[0].length, 83 + ) 84 + if (entity) { 85 + entities.push(entity) 86 + } 87 + } 88 + } 89 + 90 + return new ParseResult(message, entities, this.closeDelimiter) 91 + } 92 + 93 + /** 94 + * Process a tag's content and create an Entity 95 + * @param tagContent - The content between delimiters 96 + * @param position - The position of the tag in the original message 97 + * @param endPosition - The position after the closing delimiter 98 + * @returns Entity or null if tag is malformed 99 + */ 100 + private processTag( 101 + tagContent: string, 102 + position: number, 103 + endPosition: number, 104 + ): Entity | null { 105 + // Find the type separator 106 + const separatorIndex = tagContent.indexOf(this.typeSeparator) 107 + 108 + let type: string 109 + let value: string 110 + 111 + if (separatorIndex === -1) { 112 + // No separator - treat entire content as value with empty type 113 + type = '' 114 + value = tagContent 115 + } else { 116 + // Split by separator 117 + type = tagContent.substring(0, separatorIndex) 118 + value = tagContent.substring(separatorIndex + 1) 119 + } 120 + 121 + // Parse the value and get typed result 122 + const { parsedValue, inferredType } = this.parseValue(type, value) 123 + 124 + // Apply formatter to get formatted value 125 + const formattedValue = this.applyFormatter(type, parsedValue) 126 + 127 + return { 128 + type, 129 + value, 130 + parsedValue, 131 + formattedValue, 132 + inferredType, 133 + position, 134 + endPosition, 135 + } 136 + } 137 + 138 + /** 139 + * Infer the primitive type from a raw string value 140 + * @param value - The raw string value 141 + * @returns The inferred primitive type 142 + */ 143 + private inferType(value: string): PrimitiveType { 144 + // Check for number (including decimals and negatives) 145 + if (/^-?\d+(\.\d+)?$/.test(value)) { 146 + return 'number' 147 + } 148 + 149 + // Check for boolean (case-insensitive) 150 + const lowerValue = value.toLowerCase() 151 + if (lowerValue === 'true' || lowerValue === 'false') { 152 + return 'boolean' 153 + } 154 + 155 + // Default to string 156 + return 'string' 157 + } 158 + 159 + /** 160 + * Parse a value using schema (if available) or type inference 161 + * @param type - The entity type 162 + * @param rawValue - The raw string value 163 + * @returns Object with parsedValue and inferredType 164 + */ 165 + private parseValue( 166 + type: string, 167 + rawValue: string, 168 + ): { 169 + parsedValue: string | number | boolean 170 + inferredType: PrimitiveType 171 + } { 172 + let targetType: PrimitiveType 173 + 174 + // Check if type is in schema 175 + if (this.schema && type in this.schema) { 176 + const schemaEntry = this.schema[type] 177 + // Handle both shorthand (just type) and full definition 178 + targetType = 179 + typeof schemaEntry === 'string' ? schemaEntry : schemaEntry.type 180 + } else { 181 + // Use inference for unknown types 182 + targetType = this.inferType(rawValue) 183 + } 184 + 185 + // Parse based on target type 186 + let parsedValue: string | number | boolean 187 + 188 + switch (targetType) { 189 + case 'number': 190 + parsedValue = parseFloat(rawValue) 191 + break 192 + case 'boolean': 193 + parsedValue = rawValue.toLowerCase() === 'true' 194 + break 195 + case 'string': 196 + parsedValue = rawValue 197 + break 198 + default: 199 + parsedValue = rawValue 200 + break 201 + } 202 + 203 + return { 204 + parsedValue, 205 + inferredType: targetType, 206 + } 207 + } 208 + 209 + /** 210 + * Apply formatter function to a parsed value 211 + * @param type - The entity type 212 + * @param parsedValue - The parsed value 213 + * @returns Formatted string 214 + */ 215 + private applyFormatter( 216 + type: string, 217 + parsedValue: string | number | boolean, 218 + ): string { 219 + // Check if schema has a formatter for this type 220 + if (this.schema && type in this.schema) { 221 + const schemaEntry = this.schema[type] 222 + 223 + // Only full EntityDefinition can have a formatter 224 + if (typeof schemaEntry !== 'string' && schemaEntry.format) { 225 + return schemaEntry.format(parsedValue) 226 + } 227 + } 228 + 229 + // No formatter - convert to string 230 + return String(parsedValue) 231 + } 232 + }
+305
src/examples.ts
··· 1 + /** 2 + * Example usage of TaggedStringParser 3 + * Demonstrates basic usage, schema definition, formatters, and type inference 4 + * 5 + * Run with: node src/examples.ts 6 + */ 7 + 8 + import { TaggedStringParser } from './TaggedStringParser.ts' 9 + import type { EntitySchema } from './types.ts' 10 + 11 + // ============================================================================ 12 + // Example 1: Basic Usage with Schema and Simple String Formatters 13 + // ============================================================================ 14 + 15 + console.log('=== Example 1: Basic Usage with String Formatters ===\n') 16 + 17 + const basicSchema: EntitySchema = { 18 + operation: { type: 'string', format: (v) => v.toUpperCase() }, 19 + stack: { type: 'string', format: (v) => v.trim() }, 20 + changes: { type: 'number', format: (n) => `${n} changes` }, 21 + status: 'string', // Shorthand without formatter 22 + } 23 + 24 + const basicParser = new TaggedStringParser({ schema: basicSchema }) 25 + const result1 = basicParser.parse( 26 + '[operation:deploy] started with [changes:5] to [stack: prod-stack ]', 27 + ) 28 + 29 + console.log('Original message:', result1.originalMessage) 30 + console.log('Formatted message:', result1.format()) 31 + console.log('\nParsed entities:') 32 + result1.entities.forEach((entity) => { 33 + console.log(` - Type: ${entity.type}`) 34 + console.log(` Raw value: "${entity.value}"`) 35 + console.log( 36 + ` Parsed value: ${entity.parsedValue} (${entity.inferredType})`, 37 + ) 38 + console.log(` Formatted value: "${entity.formattedValue}"`) 39 + console.log(` Position: ${entity.position}`) 40 + }) 41 + 42 + // ============================================================================ 43 + // Example 2: Automatic Type Inference for Unknown Entities 44 + // ============================================================================ 45 + 46 + console.log('\n\n=== Example 2: Automatic Type Inference ===\n') 47 + 48 + const inferenceParser = new TaggedStringParser() // No schema 49 + const result2 = inferenceParser.parse( 50 + '[count:42] items processed, [enabled:true] flag set, [name:test-service]', 51 + ) 52 + 53 + console.log('Message:', result2.originalMessage) 54 + console.log('\nInferred types:') 55 + result2.entities.forEach((entity) => { 56 + console.log( 57 + ` - [${entity.type}:${entity.value}] → ${entity.inferredType} (${typeof entity.parsedValue})`, 58 + ) 59 + console.log(` Parsed value: ${JSON.stringify(entity.parsedValue)}`) 60 + }) 61 + 62 + // ============================================================================ 63 + // Example 3: Entity Filtering by Type 64 + // ============================================================================ 65 + 66 + console.log('\n\n=== Example 3: Entity Filtering ===\n') 67 + 68 + const filterSchema: EntitySchema = { 69 + resource: 'string', 70 + action: 'string', 71 + count: 'number', 72 + } 73 + 74 + const filterParser = new TaggedStringParser({ schema: filterSchema }) 75 + const result3 = filterParser.parse( 76 + '[action:create] [resource:function] with [count:3] instances, [action:update] [resource:database]', 77 + ) 78 + 79 + console.log('All entity types:', result3.getAllTypes()) 80 + console.log( 81 + '\nActions:', 82 + result3.getEntitiesByType('action').map((e) => e.parsedValue), 83 + ) 84 + console.log( 85 + 'Resources:', 86 + result3.getEntitiesByType('resource').map((e) => e.parsedValue), 87 + ) 88 + console.log( 89 + 'Counts:', 90 + result3.getEntitiesByType('count').map((e) => e.parsedValue), 91 + ) 92 + console.log('Non-existent type:', result3.getEntitiesByType('missing')) 93 + 94 + // ============================================================================ 95 + // Example 4: IaC Logging Examples (from design document) 96 + // ============================================================================ 97 + 98 + console.log('\n\n=== Example 4: IaC Logging Examples ===\n') 99 + 100 + const iacSchema: EntitySchema = { 101 + operation: { type: 'string', format: (v) => `OP:${v}` }, 102 + stack: { type: 'string', format: (v) => `Stack(${v})` }, 103 + changes: { type: 'number', format: (n) => `${n} change(s)` }, 104 + blueprint: { type: 'string', format: (v) => `BP:${v}` }, 105 + create: 'number', 106 + update: 'number', 107 + destroy: 'number', 108 + action: 'string', 109 + resource: 'string', 110 + resourceName: 'string', 111 + type: 'string', 112 + externalId: 'string', 113 + reason: 'string', 114 + error: 'string', 115 + } 116 + 117 + const iacParser = new TaggedStringParser({ schema: iacSchema }) 118 + 119 + // Operation lifecycle 120 + console.log('Operation Lifecycle:') 121 + const op1 = iacParser.parse( 122 + '[operation:OP-123] started with [changes:5] to [stack:ST-456]', 123 + ) 124 + console.log(' ', op1.format()) 125 + 126 + const op2 = iacParser.parse( 127 + '[operation:OP-123] completed [changes:5] to [stack:ST-456]', 128 + ) 129 + console.log(' ', op2.format()) 130 + 131 + const op3 = iacParser.parse('[operation:OP-123] failed: [reason:Timeout error]') 132 + console.log(' ', op3.format()) 133 + 134 + // Planning 135 + console.log('\nPlanning:') 136 + const plan1 = iacParser.parse('[blueprint:BP-123] planning for [stack:ST-456]') 137 + console.log(' ', plan1.format()) 138 + 139 + const plan2 = iacParser.parse( 140 + '[blueprint:BP-123] plan complete with [create:2] [update:3] [destroy:1] for [stack:ST-456]', 141 + ) 142 + console.log(' ', plan2.format()) 143 + console.log(' Plan summary:', { 144 + create: plan2.getEntitiesByType('create')[0]?.parsedValue, 145 + update: plan2.getEntitiesByType('update')[0]?.parsedValue, 146 + destroy: plan2.getEntitiesByType('destroy')[0]?.parsedValue, 147 + }) 148 + 149 + // Resource commands 150 + console.log('\nResource Commands:') 151 + const res1 = iacParser.parse( 152 + '[action:create] executing for [resource:RS-123] [resourceName:my-function] [type:function]', 153 + ) 154 + console.log(' ', res1.format()) 155 + 156 + const res2 = iacParser.parse( 157 + '[action:create] completed for [resource:RS-123] [externalId:EXT-789]', 158 + ) 159 + console.log(' ', res2.format()) 160 + 161 + const res3 = iacParser.parse( 162 + '[action:create] failed for [resource:RS-123]: [error:Connection timeout]', 163 + ) 164 + console.log(' ', res3.format()) 165 + 166 + // ============================================================================ 167 + // Example 5: Custom Configuration (Delimiters with Formatting) 168 + // ============================================================================ 169 + 170 + console.log('\n\n=== Example 5: Custom Delimiters with Formatting ===\n') 171 + 172 + // This example demonstrates the fix for custom delimiter formatting 173 + // Previously, format() was hardcoded to use ']' and wouldn't work with custom delimiters 174 + // Now it correctly uses the configured delimiters to reconstruct messages 175 + 176 + const customParser = new TaggedStringParser({ 177 + openDelimiter: '{{', 178 + closeDelimiter: '}}', 179 + typeSeparator: '=', 180 + schema: { 181 + user: { type: 'string', format: (v) => `@${v}` }, 182 + count: { type: 'number', format: (v) => String(v) }, 183 + }, 184 + }) 185 + 186 + const result5 = customParser.parse( 187 + 'User {{user=john}} performed {{count=10}} actions', 188 + ) 189 + console.log('Original message:', result5.originalMessage) 190 + console.log('Formatted message:', result5.format()) 191 + console.log(' ✓ Custom delimiters {{}} work correctly with format()') 192 + console.log(' ✓ Formatters applied: user → @john, count → 10') 193 + 194 + console.log('\nParsed entities:') 195 + result5.entities.forEach((entity) => { 196 + console.log( 197 + ` - {{${entity.type}=${entity.value}}} at position ${entity.position}-${entity.endPosition}`, 198 + ) 199 + console.log(` Formatted as: "${entity.formattedValue}"`) 200 + }) 201 + 202 + // Additional custom delimiter examples 203 + console.log('\nOther custom delimiter configurations:') 204 + 205 + const angleParser = new TaggedStringParser({ 206 + openDelimiter: '<<', 207 + closeDelimiter: '>>', 208 + typeSeparator: ':', 209 + schema: { 210 + status: { type: 'string', format: (v) => v.toUpperCase() }, 211 + }, 212 + }) 213 + const angleResult = angleParser.parse('Operation <<status:success>> completed') 214 + console.log(' Angle brackets:', angleResult.format()) 215 + 216 + const parenParser = new TaggedStringParser({ 217 + openDelimiter: '(', 218 + closeDelimiter: ')', 219 + typeSeparator: ':', 220 + schema: { 221 + code: { type: 'number', format: (n) => `#${n}` }, 222 + }, 223 + }) 224 + const parenResult = parenParser.parse('Error (code:404) occurred') 225 + console.log(' Parentheses:', parenResult.format()) 226 + 227 + // ============================================================================ 228 + // Example 6: Mixed Known and Unknown Entities 229 + // ============================================================================ 230 + 231 + console.log('\n\n=== Example 6: Mixed Known and Unknown Entities ===\n') 232 + 233 + const mixedSchema: EntitySchema = { 234 + operation: { type: 'string', format: (v) => `[OP: ${v}]` }, 235 + // Other entity types will be inferred 236 + } 237 + 238 + const mixedParser = new TaggedStringParser({ schema: mixedSchema }) 239 + const result6 = mixedParser.parse( 240 + '[operation:deploy] with [timeout:30] seconds and [retry:true] flag', 241 + ) 242 + 243 + console.log('Formatted:', result6.format()) 244 + console.log('\nEntity details:') 245 + result6.entities.forEach((entity) => { 246 + const source = mixedSchema[entity.type] ? 'schema' : 'inferred' 247 + console.log( 248 + ` - ${entity.type}: ${entity.parsedValue} (${entity.inferredType}, ${source})`, 249 + ) 250 + }) 251 + 252 + // ============================================================================ 253 + // Example 7: Accessing Entity Properties 254 + // ============================================================================ 255 + 256 + console.log('\n\n=== Example 7: Accessing Entity Properties ===\n') 257 + 258 + const propsSchema: EntitySchema = { 259 + price: { type: 'number', format: (n) => `$${n.toFixed(2)}` }, 260 + available: { type: 'boolean', format: (b) => (b ? '✓' : '✗') }, 261 + product: 'string', 262 + } 263 + 264 + const propsParser = new TaggedStringParser({ schema: propsSchema }) 265 + const result7 = propsParser.parse( 266 + '[product:Widget] is [available:true] at [price:29.99]', 267 + ) 268 + 269 + console.log('Original message:', result7.originalMessage) 270 + console.log('Formatted message:', result7.format()) 271 + console.log('\nDetailed entity properties:') 272 + result7.entities.forEach((entity) => { 273 + console.log(`\n Entity: ${entity.type}`) 274 + console.log(` value (raw string): "${entity.value}"`) 275 + console.log( 276 + ` parsedValue (typed): ${JSON.stringify(entity.parsedValue)} (${typeof entity.parsedValue})`, 277 + ) 278 + console.log(` formattedValue (display): "${entity.formattedValue}"`) 279 + console.log(` inferredType: ${entity.inferredType}`) 280 + console.log(` position: ${entity.position}`) 281 + }) 282 + 283 + // ============================================================================ 284 + // Example 8: Handling Quoted Values 285 + // ============================================================================ 286 + 287 + console.log('\n\n=== Example 8: Handling Quoted Values ===\n') 288 + 289 + const quotedParser = new TaggedStringParser({ 290 + schema: { 291 + resourceName: 'string', 292 + error: 'string', 293 + }, 294 + }) 295 + 296 + const result8 = quotedParser.parse( 297 + '[resourceName:"my-function"] failed with [error:"Connection timeout"]', 298 + ) 299 + console.log('Original:', result8.originalMessage) 300 + console.log('\nEntity values (quotes preserved):') 301 + result8.entities.forEach((entity) => { 302 + console.log(` ${entity.type}: ${entity.value}`) 303 + }) 304 + 305 + console.log('\n=== Examples Complete ===\n')
+66
src/types.ts
··· 1 + /** 2 + * Primitive types supported by the parser 3 + */ 4 + export type PrimitiveType = 'string' | 'number' | 'boolean' 5 + 6 + /** 7 + * Entity definition with optional formatter function 8 + */ 9 + export interface EntityDefinition { 10 + type: PrimitiveType 11 + format?: (value: unknown) => string 12 + } 13 + 14 + /** 15 + * Schema mapping entity type names to their definitions 16 + * Can use shorthand (just the type) or full definition with formatter 17 + */ 18 + export type EntitySchema = Record<string, PrimitiveType | EntityDefinition> 19 + 20 + /** 21 + * Parsed entity extracted from a string 22 + */ 23 + export interface Entity { 24 + type: string 25 + value: string 26 + parsedValue: string | number | boolean 27 + formattedValue: string 28 + inferredType: PrimitiveType 29 + /** The starting position of the tag in the original message */ 30 + position: number 31 + /** The ending position of the tag in the original message (after the closing delimiter) */ 32 + endPosition: number 33 + } 34 + 35 + /** 36 + * Configuration options for the parser 37 + */ 38 + export interface ParserConfig { 39 + openDelimiter?: string 40 + closeDelimiter?: string 41 + typeSeparator?: string 42 + schema?: EntitySchema 43 + } 44 + 45 + /** 46 + * Result of parsing a string 47 + */ 48 + export interface ParseResult { 49 + originalMessage: string 50 + entities: Entity[] 51 + 52 + /** 53 + * Get all entities of a specific type 54 + */ 55 + getEntitiesByType(type: string): Entity[] 56 + 57 + /** 58 + * Get all unique entity types found in the message 59 + */ 60 + getAllTypes(): string[] 61 + 62 + /** 63 + * Reconstruct the message with formatted entity values 64 + */ 65 + format(): string 66 + }
+17
tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ES2022", 4 + "module": "ESNext", 5 + "moduleResolution": "node", 6 + "lib": ["ES2022"], 7 + "strict": true, 8 + "esModuleInterop": true, 9 + "skipLibCheck": true, 10 + "allowImportingTsExtensions": true, 11 + "forceConsistentCasingInFileNames": true, 12 + "resolveJsonModule": true, 13 + "noEmit": true 14 + }, 15 + "include": ["src/**/*"], 16 + "exclude": ["node_modules"] 17 + }