littlewing 0.9.2 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -596,6 +596,94 @@ declare class Executor {
596
596
  */
597
597
  declare function execute(input: string | ASTNode, context?: ExecutionContext): RuntimeValue;
598
598
  /**
599
+ * Options for customizing the humanization output
600
+ */
601
+ interface HumanizeOptions {
602
+ /**
603
+ * Enable HTML output with wrapper tags
604
+ * @default false
605
+ */
606
+ html?: boolean;
607
+ /**
608
+ * CSS classes to apply to different node types when html is true
609
+ */
610
+ htmlClasses?: {
611
+ number?: string;
612
+ identifier?: string;
613
+ operator?: string;
614
+ function?: string;
615
+ };
616
+ /**
617
+ * Custom phrases for function names
618
+ * Maps function name to human-readable phrase
619
+ * Example: { 'MAX': 'the maximum of', 'CUSTOM': 'my custom function with' }
620
+ */
621
+ functionPhrases?: Record<string, string>;
622
+ /**
623
+ * Custom phrases for operators
624
+ * Overrides default operator text
625
+ */
626
+ operatorPhrases?: Partial<Record<Operator, string>>;
627
+ /**
628
+ * Use more verbose, descriptive phrasing
629
+ * @default false
630
+ */
631
+ verbose?: boolean;
632
+ }
633
+ /**
634
+ * Converts an AST node to human-readable English text
635
+ */
636
+ declare class Humanizer {
637
+ private readonly options;
638
+ private readonly operatorPhrases;
639
+ private readonly functionPhrases;
640
+ constructor(options?: HumanizeOptions);
641
+ /**
642
+ * Humanize an AST node into English text
643
+ */
644
+ humanize(node: ASTNode): string;
645
+ private humanizeProgram;
646
+ private humanizeNumberLiteral;
647
+ private humanizeIdentifier;
648
+ private humanizeBinaryOp;
649
+ private humanizeUnaryOp;
650
+ private humanizeFunctionCall;
651
+ private humanizeAssignment;
652
+ private humanizeConditionalExpression;
653
+ /**
654
+ * Wrap text in HTML tag if html option is enabled
655
+ */
656
+ private wrapHtml;
657
+ }
658
+ /**
659
+ * Humanize an AST node into human-readable English text
660
+ *
661
+ * @param node - The AST node to humanize
662
+ * @param options - Optional configuration for the output
663
+ * @returns Human-readable English text
664
+ *
665
+ * @example
666
+ * ```typescript
667
+ * const ast = parseSource('price * quantity > 100 ? (price * quantity - discount) * (1 + tax_rate) : MAX(price * quantity, 50)')
668
+ * const text = humanize(ast)
669
+ * // "if price times quantity is greater than 100 then (price times quantity minus discount) times (1 plus tax_rate), otherwise the maximum of price times quantity and 50"
670
+ * ```
671
+ *
672
+ * @example
673
+ * ```typescript
674
+ * // With HTML formatting
675
+ * const text = humanize(ast, {
676
+ * html: true,
677
+ * htmlClasses: {
678
+ * identifier: 'variable',
679
+ * operator: 'op',
680
+ * number: 'num'
681
+ * }
682
+ * })
683
+ * ```
684
+ */
685
+ declare function humanize(node: ASTNode, options?: HumanizeOptions): string;
686
+ /**
599
687
  * Lexer - converts source code into tokens
600
688
  * Implements a single-pass O(n) tokenization algorithm
601
689
  */
@@ -720,4 +808,4 @@ declare class Parser {
720
808
  * @returns Parsed AST
721
809
  */
722
810
  declare function parseSource(source: string): ASTNode;
723
- export { parseSource, optimize, isUnaryOp, isProgram, isNumberLiteral, isIdentifier, isFunctionCall, isConditionalExpression, isBinaryOp, isAssignment, generate, extractInputVariables, execute, defaultContext, exports_date_utils as dateUtils, exports_ast as ast, UnaryOp, TokenType, Token, RuntimeValue, Program, Parser, Operator, NumberLiteral, Lexer, Identifier, FunctionCall, Executor, ExecutionContext, ConditionalExpression, CodeGenerator, BinaryOp, Assignment, ASTNode };
811
+ export { parseSource, optimize, isUnaryOp, isProgram, isNumberLiteral, isIdentifier, isFunctionCall, isConditionalExpression, isBinaryOp, isAssignment, humanize, generate, extractInputVariables, execute, defaultContext, exports_date_utils as dateUtils, exports_ast as ast, UnaryOp, TokenType, Token, RuntimeValue, Program, Parser, Operator, NumberLiteral, Lexer, Identifier, Humanizer, HumanizeOptions, FunctionCall, Executor, ExecutionContext, ConditionalExpression, CodeGenerator, BinaryOp, Assignment, ASTNode };
package/dist/index.js CHANGED
@@ -1037,6 +1037,188 @@ function execute(input, context) {
1037
1037
  const executor = new Executor(context);
1038
1038
  return executor.execute(node);
1039
1039
  }
1040
+ // src/humanizer.ts
1041
+ var DEFAULT_OPERATOR_PHRASES = {
1042
+ "+": "plus",
1043
+ "-": "minus",
1044
+ "*": "times",
1045
+ "/": "divided by",
1046
+ "%": "modulo",
1047
+ "^": "to the power of",
1048
+ "==": "equals",
1049
+ "!=": "is not equal to",
1050
+ "<": "is less than",
1051
+ ">": "is greater than",
1052
+ "<=": "is less than or equal to",
1053
+ ">=": "is greater than or equal to",
1054
+ "&&": "and",
1055
+ "||": "or"
1056
+ };
1057
+ var DEFAULT_FUNCTION_PHRASES = {
1058
+ ABS: "the absolute value of",
1059
+ CEIL: "the ceiling of",
1060
+ FLOOR: "the floor of",
1061
+ ROUND: "the rounded value of",
1062
+ SQRT: "the square root of",
1063
+ MIN: "the minimum of",
1064
+ MAX: "the maximum of",
1065
+ CLAMP: "clamped",
1066
+ SIN: "the sine of",
1067
+ COS: "the cosine of",
1068
+ TAN: "the tangent of",
1069
+ LOG: "the natural logarithm of",
1070
+ LOG10: "the base-10 logarithm of",
1071
+ EXP: "e raised to the power of",
1072
+ NOW: "the current time",
1073
+ DATE: "the date",
1074
+ GET_YEAR: "the year of",
1075
+ GET_MONTH: "the month of",
1076
+ GET_DAY: "the day of",
1077
+ GET_HOUR: "the hour of",
1078
+ GET_MINUTE: "the minute of",
1079
+ GET_SECOND: "the second of",
1080
+ START_OF_DAY: "the start of day for",
1081
+ END_OF_DAY: "the end of day for",
1082
+ START_OF_MONTH: "the start of month for",
1083
+ END_OF_MONTH: "the end of month for",
1084
+ ADD_DAYS: "add days to",
1085
+ ADD_MONTHS: "add months to",
1086
+ ADD_YEARS: "add years to",
1087
+ DIFFERENCE_IN_DAYS: "the difference in days between",
1088
+ DIFFERENCE_IN_HOURS: "the difference in hours between",
1089
+ DIFFERENCE_IN_MINUTES: "the difference in minutes between",
1090
+ IS_WEEKEND: "whether it is a weekend for",
1091
+ IS_LEAP_YEAR: "whether it is a leap year for"
1092
+ };
1093
+
1094
+ class Humanizer {
1095
+ options;
1096
+ operatorPhrases;
1097
+ functionPhrases;
1098
+ constructor(options = {}) {
1099
+ this.options = options;
1100
+ this.operatorPhrases = {
1101
+ ...DEFAULT_OPERATOR_PHRASES,
1102
+ ...options.operatorPhrases
1103
+ };
1104
+ this.functionPhrases = {
1105
+ ...DEFAULT_FUNCTION_PHRASES,
1106
+ ...options.functionPhrases
1107
+ };
1108
+ }
1109
+ humanize(node) {
1110
+ if (isProgram(node)) {
1111
+ return this.humanizeProgram(node);
1112
+ }
1113
+ if (isNumberLiteral(node)) {
1114
+ return this.humanizeNumberLiteral(node);
1115
+ }
1116
+ if (isIdentifier(node)) {
1117
+ return this.humanizeIdentifier(node);
1118
+ }
1119
+ if (isBinaryOp(node)) {
1120
+ return this.humanizeBinaryOp(node);
1121
+ }
1122
+ if (isUnaryOp(node)) {
1123
+ return this.humanizeUnaryOp(node);
1124
+ }
1125
+ if (isFunctionCall(node)) {
1126
+ return this.humanizeFunctionCall(node);
1127
+ }
1128
+ if (isAssignment(node)) {
1129
+ return this.humanizeAssignment(node);
1130
+ }
1131
+ if (isConditionalExpression(node)) {
1132
+ return this.humanizeConditionalExpression(node);
1133
+ }
1134
+ throw new Error(`Unknown node type: ${JSON.stringify(node)}`);
1135
+ }
1136
+ humanizeProgram(node) {
1137
+ const statements = node.statements.map((stmt) => {
1138
+ const text = this.humanize(stmt);
1139
+ return text.charAt(0).toUpperCase() + text.slice(1);
1140
+ });
1141
+ return statements.join(". ");
1142
+ }
1143
+ humanizeNumberLiteral(node) {
1144
+ const text = String(node.value);
1145
+ return this.wrapHtml(text, "number");
1146
+ }
1147
+ humanizeIdentifier(node) {
1148
+ return this.wrapHtml(node.name, "identifier");
1149
+ }
1150
+ humanizeBinaryOp(node) {
1151
+ const left = this.humanize(node.left);
1152
+ const right = this.humanize(node.right);
1153
+ const operator = this.operatorPhrases[node.operator];
1154
+ const operatorText = this.wrapHtml(operator, "operator");
1155
+ return `${left} ${operatorText} ${right}`;
1156
+ }
1157
+ humanizeUnaryOp(node) {
1158
+ const arg = this.humanize(node.argument);
1159
+ if (node.operator === "-") {
1160
+ const needsGrouping = !isNumberLiteral(node.argument) && !isIdentifier(node.argument);
1161
+ if (needsGrouping) {
1162
+ return `the negative of ${arg}`;
1163
+ }
1164
+ return `negative ${arg}`;
1165
+ }
1166
+ if (node.operator === "!") {
1167
+ return `not ${arg}`;
1168
+ }
1169
+ throw new Error(`Unknown unary operator: ${node.operator}`);
1170
+ }
1171
+ humanizeFunctionCall(node) {
1172
+ const funcName = this.wrapHtml(node.name, "function");
1173
+ const args = node.arguments.map((arg) => this.humanize(arg));
1174
+ const phrase = this.functionPhrases[node.name];
1175
+ if (phrase) {
1176
+ if (args.length === 0) {
1177
+ return phrase;
1178
+ }
1179
+ if (args.length === 1) {
1180
+ return `${phrase} ${args[0]}`;
1181
+ }
1182
+ const lastArg2 = args[args.length - 1];
1183
+ const otherArgs2 = args.slice(0, -1).join(", ");
1184
+ return `${phrase} ${otherArgs2} and ${lastArg2}`;
1185
+ }
1186
+ if (args.length === 0) {
1187
+ return `the result of ${funcName}`;
1188
+ }
1189
+ if (args.length === 1) {
1190
+ return `the result of ${funcName} with ${args[0]}`;
1191
+ }
1192
+ const lastArg = args[args.length - 1];
1193
+ const otherArgs = args.slice(0, -1).join(", ");
1194
+ return `the result of ${funcName} with ${otherArgs} and ${lastArg}`;
1195
+ }
1196
+ humanizeAssignment(node) {
1197
+ const name = this.wrapHtml(node.name, "identifier");
1198
+ const value = this.humanize(node.value);
1199
+ return `set ${name} to ${value}`;
1200
+ }
1201
+ humanizeConditionalExpression(node) {
1202
+ const condition = this.humanize(node.condition);
1203
+ const consequent = this.humanize(node.consequent);
1204
+ const alternate = this.humanize(node.alternate);
1205
+ return `if ${condition} then ${consequent}, otherwise ${alternate}`;
1206
+ }
1207
+ wrapHtml(text, type) {
1208
+ if (!this.options.html) {
1209
+ return text;
1210
+ }
1211
+ const className = this.options.htmlClasses?.[type];
1212
+ if (className) {
1213
+ return `<span class="${className}">${text}</span>`;
1214
+ }
1215
+ return `<span>${text}</span>`;
1216
+ }
1217
+ }
1218
+ function humanize(node, options) {
1219
+ const humanizer = new Humanizer(options);
1220
+ return humanizer.humanize(node);
1221
+ }
1040
1222
  // src/optimizer.ts
1041
1223
  function optimize(node) {
1042
1224
  if (isNumberLiteral(node)) {
@@ -1099,6 +1281,7 @@ export {
1099
1281
  isConditionalExpression,
1100
1282
  isBinaryOp,
1101
1283
  isAssignment,
1284
+ humanize,
1102
1285
  generate,
1103
1286
  extractInputVariables,
1104
1287
  execute,
@@ -1108,6 +1291,7 @@ export {
1108
1291
  TokenType,
1109
1292
  Parser,
1110
1293
  Lexer,
1294
+ Humanizer,
1111
1295
  Executor,
1112
1296
  CodeGenerator
1113
1297
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "littlewing",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "description": "A minimal, high-performance arithmetic expression language with lexer, parser, and executor. Optimized for browsers with zero dependencies and type-safe execution.",
5
5
  "keywords": [
6
6
  "arithmetic",