@xeplr/nlp-parser 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Xeplr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/index.js ADDED
@@ -0,0 +1,28 @@
1
+ const { parse, parseCondition } = require('./lib/parser');
2
+ const { evaluate, evaluateCondition, evaluateWhen, resolvePath } = require('./lib/evaluator');
3
+
4
+ /**
5
+ * Parse a rule string into a structured action object.
6
+ *
7
+ * @param {string} rule - e.g. "if typeof $.header is string then <hide> [col1, col2]"
8
+ * @returns {{ action: { name, when: { lhs, op, rhs, rhsTo? }, applyOn?, value? } }}
9
+ */
10
+ function parseNLP(rule) {
11
+ return parse(rule);
12
+ }
13
+
14
+ /**
15
+ * Parse + evaluate in one call.
16
+ * Returns the action object if the condition matches, otherwise null.
17
+ *
18
+ * @param {string} rule - the rule string
19
+ * @param {*} inputs - the data object ($ in the rule)
20
+ * @returns {object|null}
21
+ */
22
+ function applyNLP(rule, inputs) {
23
+ const parsed = parse(rule);
24
+ const matches = evaluate(parsed, inputs);
25
+ return matches ? parsed.action : null;
26
+ }
27
+
28
+ module.exports = { parseNLP, applyNLP, evaluate, evaluateCondition, evaluateWhen, parseCondition, resolvePath };
@@ -0,0 +1,103 @@
1
+ const { parseCondition } = require('./parser');
2
+
3
+ /**
4
+ * Resolves a $-path against a data object.
5
+ * $.employee.department.name → data.employee.department.name
6
+ * $ → data (the root itself)
7
+ */
8
+ function resolvePath(path, data) {
9
+ const parts = path.replace(/^\$\.?/, '').split('.');
10
+ let current = data;
11
+ for (const part of parts) {
12
+ if (!part) continue;
13
+ if (current == null) return undefined;
14
+ current = current[part];
15
+ }
16
+ return current;
17
+ }
18
+
19
+ /**
20
+ * Coerce value for numeric comparisons.
21
+ * If both sides can be numbers, compare as numbers.
22
+ */
23
+ function toComparable(val) {
24
+ if (typeof val === 'number') return val;
25
+ if (typeof val === 'string' && !isNaN(val) && val !== '') return Number(val);
26
+ return val;
27
+ }
28
+
29
+ /**
30
+ * Evaluate a when condition object against data.
31
+ * when: { lhs, op, rhs, rhsTo? }
32
+ */
33
+ function evaluateWhen(when, data) {
34
+ const raw = resolvePath(when.lhs, data);
35
+ const value = toComparable(raw);
36
+ const rhs = toComparable(when.rhs);
37
+
38
+ switch (when.op) {
39
+ case 'is': return value === rhs;
40
+ case 'is_not': return value !== rhs;
41
+ case 'is_null': return raw == null || raw === '';
42
+ case 'is_not_null': return raw != null && raw !== '';
43
+ case 'typeof': return typeof raw === when.rhs;
44
+ case 'typeof_not': return typeof raw !== when.rhs;
45
+ case '>': return value > rhs;
46
+ case '<': return value < rhs;
47
+ case '>=': return value >= rhs;
48
+ case '<=': return value <= rhs;
49
+ case 'between': {
50
+ const rhsTo = toComparable(when.rhsTo);
51
+ if (rhsTo == null) return value >= rhs;
52
+ return value >= rhs && value <= rhsTo;
53
+ }
54
+ case 'in': {
55
+ const list = Array.isArray(when.rhs) ? when.rhs : [when.rhs];
56
+ return list.some(item => toComparable(item) === value);
57
+ }
58
+ case 'not_in': {
59
+ const list = Array.isArray(when.rhs) ? when.rhs : [when.rhs];
60
+ return !list.some(item => toComparable(item) === value);
61
+ }
62
+ case 'starts_with': {
63
+ if (raw == null) return false;
64
+ return String(raw).toLowerCase().startsWith(String(when.rhs).toLowerCase());
65
+ }
66
+ case 'ends_with': {
67
+ if (raw == null) return false;
68
+ return String(raw).toLowerCase().endsWith(String(when.rhs).toLowerCase());
69
+ }
70
+ case 'contains': {
71
+ if (raw == null) return false;
72
+ return String(raw).toLowerCase().includes(String(when.rhs).toLowerCase());
73
+ }
74
+ case 'not_contains': {
75
+ if (raw == null) return false;
76
+ return !String(raw).toLowerCase().includes(String(when.rhs).toLowerCase());
77
+ }
78
+ default: return false;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Evaluates a parsed rule's condition against actual data.
84
+ * Returns true if the condition matches.
85
+ */
86
+ function evaluate(parsed, data) {
87
+ return evaluateWhen(parsed.action.when, data);
88
+ }
89
+
90
+ /**
91
+ * Parse + evaluate a standalone condition string against data.
92
+ * No "if"/"then" needed — just the condition.
93
+ *
94
+ * @param {string} conditionStr - e.g. "$.status is active", "$.price > 100"
95
+ * @param {object} data - the data object
96
+ * @returns {boolean}
97
+ */
98
+ function evaluateCondition(conditionStr, data) {
99
+ var when = parseCondition(conditionStr);
100
+ return evaluateWhen(when, data);
101
+ }
102
+
103
+ module.exports = { evaluate, evaluateCondition, evaluateWhen, resolvePath };
package/lib/parser.js ADDED
@@ -0,0 +1,188 @@
1
+ const { tokenize } = require('./tokenizer');
2
+
3
+ /**
4
+ * Create a token walker with peek/advance/expect helpers.
5
+ */
6
+ function createWalker(tokens) {
7
+ var pos = 0;
8
+ function peek() { return tokens[pos] || null; }
9
+ function advance() { return tokens[pos++]; }
10
+ function expect(type, value) {
11
+ const t = advance();
12
+ if (!t) throw new Error(`Unexpected end of rule, expected ${type}${value ? ' ' + value : ''}`);
13
+ if (t.type !== type || (value !== undefined && t.value !== value)) {
14
+ throw new Error(`Expected ${type} "${value || ''}", got ${t.type} "${t.value}"`);
15
+ }
16
+ return t;
17
+ }
18
+ return { peek, advance, expect };
19
+ }
20
+
21
+ /**
22
+ * Parse a condition from the token stream.
23
+ * condition := ['typeof'] path operator rhs ['and' rhs2]
24
+ *
25
+ * Returns { lhs, op, rhs, rhsTo? }
26
+ */
27
+ function parseConditionTokens(w) {
28
+ // optional typeof
29
+ var usesTypeof = false;
30
+ if (w.peek() && w.peek().type === 'KEYWORD' && w.peek().value === 'typeof') {
31
+ w.advance();
32
+ usesTypeof = true;
33
+ }
34
+
35
+ var path = w.expect('PATH');
36
+
37
+ // operator
38
+ var op;
39
+ var opToken = w.peek();
40
+
41
+ if (opToken && opToken.type === 'OP') {
42
+ op = w.advance().value;
43
+ if (usesTypeof) op = 'typeof_' + op;
44
+ } else if (opToken && opToken.type === 'KEYWORD' && opToken.value === 'is') {
45
+ w.advance();
46
+ var negated = false;
47
+ if (w.peek() && w.peek().type === 'KEYWORD' && w.peek().value === 'not') {
48
+ w.advance();
49
+ negated = true;
50
+ }
51
+ if (w.peek() && w.peek().type === 'KEYWORD' && w.peek().value === 'null') {
52
+ w.advance();
53
+ op = negated ? 'is_not_null' : 'is_null';
54
+ } else {
55
+ op = usesTypeof ? 'typeof' : 'is';
56
+ if (negated) op += '_not';
57
+ }
58
+ } else if (opToken && opToken.type === 'KEYWORD' && opToken.value === 'not') {
59
+ w.advance();
60
+ if (w.peek() && w.peek().type === 'KEYWORD' && w.peek().value === 'in') {
61
+ w.advance();
62
+ op = 'not_in';
63
+ } else if (w.peek() && w.peek().type === 'KEYWORD' && w.peek().value === 'contains') {
64
+ w.advance();
65
+ op = 'not_contains';
66
+ } else {
67
+ throw new Error(`Expected "in" or "contains" after "not", got ${w.peek() ? w.peek().value : 'end of input'}`);
68
+ }
69
+ } else if (opToken && opToken.type === 'KEYWORD' && opToken.value === 'in') {
70
+ w.advance();
71
+ op = 'in';
72
+ } else if (opToken && opToken.type === 'KEYWORD' && opToken.value === 'starts') {
73
+ w.advance();
74
+ w.expect('KEYWORD', 'with');
75
+ op = 'starts_with';
76
+ } else if (opToken && opToken.type === 'KEYWORD' && opToken.value === 'ends') {
77
+ w.advance();
78
+ w.expect('KEYWORD', 'with');
79
+ op = 'ends_with';
80
+ } else if (opToken && opToken.type === 'KEYWORD' && opToken.value === 'contains') {
81
+ w.advance();
82
+ op = 'contains';
83
+ } else if (opToken && opToken.type === 'KEYWORD' && opToken.value === 'does') {
84
+ w.advance();
85
+ w.expect('KEYWORD', 'not');
86
+ // accept both "contain" and "contains"
87
+ var containToken = w.peek();
88
+ if (containToken && containToken.type === 'KEYWORD' && (containToken.value === 'contains' || containToken.value === 'contain')) {
89
+ w.advance();
90
+ } else {
91
+ throw new Error(`Expected "contain" or "contains" after "does not", got ${containToken ? containToken.value : 'end of input'}`);
92
+ }
93
+ op = 'not_contains';
94
+ } else if (opToken && opToken.type === 'KEYWORD' && opToken.value === 'between') {
95
+ w.advance();
96
+ op = 'between';
97
+ } else {
98
+ throw new Error(`Expected operator, got ${opToken ? opToken.type + ' ' + opToken.value : 'end of input'}`);
99
+ }
100
+
101
+ // rhs
102
+ var rhs = null;
103
+ var rhsTo = null;
104
+ if (op !== 'is_null' && op !== 'is_not_null') {
105
+ var rhsToken = w.peek();
106
+ if (rhsToken && rhsToken.type === 'STRING') {
107
+ rhs = w.advance().value;
108
+ } else if (rhsToken && rhsToken.type === 'NUMBER') {
109
+ rhs = w.advance().value;
110
+ } else if (rhsToken && rhsToken.type === 'IDENTIFIER') {
111
+ rhs = w.advance().value;
112
+ } else if (rhsToken && rhsToken.type === 'ARRAY') {
113
+ rhs = w.advance().value;
114
+ } else {
115
+ throw new Error(`Expected value after operator, got ${rhsToken ? rhsToken.type : 'end of input'}`);
116
+ }
117
+
118
+ // between X and Y
119
+ if (op === 'between' && w.peek() && w.peek().type === 'KEYWORD' && w.peek().value === 'and') {
120
+ w.advance();
121
+ var toToken = w.peek();
122
+ if (toToken && (toToken.type === 'NUMBER' || toToken.type === 'STRING' || toToken.type === 'IDENTIFIER')) {
123
+ rhsTo = w.advance().value;
124
+ } else {
125
+ throw new Error(`Expected second value after "and" in between, got ${toToken ? toToken.type : 'end of input'}`);
126
+ }
127
+ }
128
+ }
129
+
130
+ var when = { lhs: path.value, op: op, rhs: rhs };
131
+ if (rhsTo !== null) when.rhsTo = rhsTo;
132
+ return when;
133
+ }
134
+
135
+ /**
136
+ * Parse a full rule: "if <condition> then <action/return>"
137
+ */
138
+ function parse(rule) {
139
+ var tokens = tokenize(rule);
140
+ var w = createWalker(tokens);
141
+
142
+ w.expect('KEYWORD', 'if');
143
+ var when = parseConditionTokens(w);
144
+ w.expect('KEYWORD', 'then');
145
+
146
+ // result := '<action>' '[targets]' | 'return' value
147
+ var next = w.peek();
148
+ if (!next) throw new Error('Unexpected end of rule after "then"');
149
+
150
+ if (next.type === 'ACTION') {
151
+ var actionName = w.advance().value;
152
+ var applyOn = [];
153
+ if (w.peek() && w.peek().type === 'ARRAY') {
154
+ applyOn = w.advance().value;
155
+ }
156
+ return { action: { name: actionName, when: when, applyOn: applyOn } };
157
+ }
158
+
159
+ if (next.type === 'KEYWORD' && next.value === 'return') {
160
+ w.advance();
161
+ var valToken = w.peek();
162
+ var value;
163
+ if (valToken && valToken.type === 'STRING') {
164
+ value = w.advance().value;
165
+ } else if (valToken && valToken.type === 'IDENTIFIER') {
166
+ value = w.advance().value;
167
+ } else {
168
+ throw new Error(`Expected return value, got ${valToken ? valToken.type : 'end of input'}`);
169
+ }
170
+ return { action: { name: 'return', when: when, value: value } };
171
+ }
172
+
173
+ throw new Error(`Expected <action> or return, got ${next.type} "${next.value}"`);
174
+ }
175
+
176
+ /**
177
+ * Parse a standalone condition string (no "if"/"then" needed).
178
+ * Input: "$.field is active" or "$.price > 100" or "$.name starts with 'Jo'"
179
+ *
180
+ * Returns: { lhs, op, rhs, rhsTo? }
181
+ */
182
+ function parseCondition(conditionStr) {
183
+ var tokens = tokenize(conditionStr);
184
+ var w = createWalker(tokens);
185
+ return parseConditionTokens(w);
186
+ }
187
+
188
+ module.exports = { parse, parseCondition };
@@ -0,0 +1,113 @@
1
+ const KEYWORDS = new Set(['if', 'typeof', 'is', 'not', 'in', 'then', 'return', 'and', 'or', 'null', 'starts', 'ends', 'with', 'contains', 'contain', 'does', 'between']);
2
+
3
+ function tokenize(input) {
4
+ const tokens = [];
5
+ let i = 0;
6
+
7
+ while (i < input.length) {
8
+ // whitespace
9
+ if (/\s/.test(input[i])) { i++; continue; }
10
+
11
+ // path: $...
12
+ if (input[i] === '$') {
13
+ let path = '';
14
+ while (i < input.length && /[a-zA-Z0-9_.$]/.test(input[i])) {
15
+ path += input[i++];
16
+ }
17
+ tokens.push({ type: 'PATH', value: path });
18
+ continue;
19
+ }
20
+
21
+ // comparison operators: >=, <=, >, <
22
+ // distinguish from <action> by peeking: if '<' is followed by '=' it's <=
23
+ // if '<' is followed by a word and then '>' it's an action
24
+ if (input[i] === '>' && input[i + 1] === '=') {
25
+ tokens.push({ type: 'OP', value: '>=' }); i += 2; continue;
26
+ }
27
+ if (input[i] === '>') {
28
+ tokens.push({ type: 'OP', value: '>' }); i++; continue;
29
+ }
30
+ if (input[i] === '<' && input[i + 1] === '=') {
31
+ tokens.push({ type: 'OP', value: '<=' }); i += 2; continue;
32
+ }
33
+ if (input[i] === '<') {
34
+ // peek ahead: if content until '>' looks like a word, it's an <action>
35
+ let j = i + 1;
36
+ let candidate = '';
37
+ while (j < input.length && input[j] !== '>' && input[j] !== '<' && input[j] !== '\n') {
38
+ candidate += input[j++];
39
+ }
40
+ if (j < input.length && input[j] === '>' && /^[a-zA-Z_][\w-]*$/.test(candidate.trim())) {
41
+ // it's an <action>
42
+ i++; // skip <
43
+ let name = '';
44
+ while (i < input.length && input[i] !== '>') {
45
+ name += input[i++];
46
+ }
47
+ if (i < input.length) i++; // skip >
48
+ tokens.push({ type: 'ACTION', value: name.trim() });
49
+ continue;
50
+ }
51
+ // otherwise it's the < operator
52
+ tokens.push({ type: 'OP', value: '<' }); i++; continue;
53
+ }
54
+
55
+ // array: [a, b, c]
56
+ if (input[i] === '[') {
57
+ i++;
58
+ let content = '';
59
+ while (i < input.length && input[i] !== ']') {
60
+ content += input[i++];
61
+ }
62
+ if (i < input.length) i++; // skip ]
63
+ const items = content.split(',').map(s => s.trim()).filter(Boolean);
64
+ tokens.push({ type: 'ARRAY', value: items });
65
+ continue;
66
+ }
67
+
68
+ // string literal: 'value' or "value"
69
+ if (input[i] === "'" || input[i] === '"') {
70
+ const quote = input[i++];
71
+ let str = '';
72
+ while (i < input.length && input[i] !== quote) {
73
+ str += input[i++];
74
+ }
75
+ if (i < input.length) i++; // skip closing quote
76
+ tokens.push({ type: 'STRING', value: str });
77
+ continue;
78
+ }
79
+
80
+ // number: 123, 3.14, -5
81
+ if (/[0-9]/.test(input[i]) || (input[i] === '-' && i + 1 < input.length && /[0-9]/.test(input[i + 1]))) {
82
+ let num = '';
83
+ if (input[i] === '-') { num += input[i++]; }
84
+ while (i < input.length && /[0-9.]/.test(input[i])) {
85
+ num += input[i++];
86
+ }
87
+ tokens.push({ type: 'NUMBER', value: Number(num) });
88
+ continue;
89
+ }
90
+
91
+ // word: keyword or identifier
92
+ if (/[a-zA-Z_]/.test(input[i])) {
93
+ let word = '';
94
+ while (i < input.length && /[a-zA-Z0-9_]/.test(input[i])) {
95
+ word += input[i++];
96
+ }
97
+ const lower = word.toLowerCase();
98
+ if (KEYWORDS.has(lower)) {
99
+ tokens.push({ type: 'KEYWORD', value: lower });
100
+ } else {
101
+ tokens.push({ type: 'IDENTIFIER', value: word });
102
+ }
103
+ continue;
104
+ }
105
+
106
+ // skip unknown characters
107
+ i++;
108
+ }
109
+
110
+ return tokens;
111
+ }
112
+
113
+ module.exports = { tokenize };
package/package.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@xeplr/nlp-parser",
3
+ "version": "1.0.0",
4
+ "description": "Isomorphic NLP rule parser — parses a simple DSL into structured action objects",
5
+ "main": "index.js",
6
+ "files": ["index.js", "lib/"],
7
+ "keywords": ["nlp", "parser", "dsl", "isomorphic"],
8
+ "author": "xeplr",
9
+ "license": "MIT",
10
+ "repository": { "type": "git", "url": "https://github.com/Xeplr/xeplr-nlp-parser" },
11
+ "publishConfig": { "access": "public" }
12
+ }