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 +89 -1
- package/dist/index.js +184 -0
- package/package.json +1 -1
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.
|
|
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",
|