@webqit/subscript
Advanced tools
Comparing version 1.1.11 to 2.0.0
@@ -11,3 +11,3 @@ { | ||
"homepage": "https://webqit.io/tooling/subscript", | ||
"version": "1.1.11", | ||
"version": "2.0.0", | ||
"license": "MIT", | ||
@@ -22,3 +22,3 @@ "repository": { | ||
"type": "module", | ||
"sideEffects": false, | ||
"sideEffects": true, | ||
"main": "./src/index.js", | ||
@@ -29,3 +29,4 @@ "scripts": { | ||
"build": "webpack --config ./webpack.config.cjs", | ||
"preversion": "npm run test && npm run build && git add -A dist", | ||
"-preversion": "npm run test && npm run build && git add -A dist", | ||
"preversion": "npm run build && git add -A dist", | ||
"postversion": "npm publish", | ||
@@ -35,11 +36,13 @@ "postpublish": "git push && git push --tags" | ||
"dependencies": { | ||
"@webqit/util": "^0.8.8" | ||
"acorn": "^8.7.0", | ||
"astring": "^1.8.1" | ||
}, | ||
"devDependencies": { | ||
"chai": "^4.3.4", | ||
"compression-webpack-plugin": "^9.2.0", | ||
"coveralls": "^3.1.1", | ||
"mocha": "^9.0.2", | ||
"mocha-lcov-reporter": "^1.3.0", | ||
"webpack": "^4.44.2", | ||
"webpack-cli": "^3.3.12" | ||
"webpack": "^5.69.0", | ||
"webpack-cli": "^4.9.2" | ||
}, | ||
@@ -46,0 +49,0 @@ "author": "Oxford Harrison <oxharris.dev@gmail.com>", |
538
README.md
@@ -9,231 +9,533 @@ # Subscript | ||
Subscript is a light-weight JavaScript parser and interpreter written in JavaScript; Subscript provides a completely-bendable JavaScript runtime for ambitious usecases. | ||
Subscript is a reactivity runtime for JavaScript. It takes any valid JavaScript code, reads its dependency graph, and gives you the mechanism to run it both in whole and in selected parts, called dependency threads. | ||
## Overview | ||
## What's A Dependency Thread? | ||
You can `parse()` a JavaScript expression `(2 + 2) * 3` into a *Subscript AST*, | ||
That's simply the line of dependencies involving two or more expressions. | ||
```js | ||
var subscript = Subscript.parse(expr); | ||
let count = 10, doubleCount = count * 2, quadCount = doubleCount * 2; | ||
``` | ||
then, `stringify()` the *Subscript AST* back into its original JavaScript expression `(2 + 2) * 3`, | ||
We just expressed that `doubleCount` should be two times the value of `count`, and that `quadCount` should be two times the value of `doubleCount`. | ||
```js | ||
let expr = subscript.stringify(); | ||
console,log( count, doubleCount, quadCount ); | ||
< 10, 20, 40 | ||
``` | ||
or even `eval()` the JavaScript expression. | ||
Problem is: this mathematical relationship only holds for as long as nothing changes. Should the value of `count` change, then its dependents would be out of sync. | ||
```js | ||
let result = subscript.eval(); | ||
// 12 | ||
count ++; | ||
``` | ||
### Features | ||
```js | ||
console,log( count, doubleCount, quadCount ); | ||
< 11, 20, 40 | ||
``` | ||
#### Small! Fast! | ||
This is that reminder that expressions in JavaScript aren't automatically bound to their dependencies. And that's what we'd expect of any programming language. | ||
Being an implementation of a subset of JavaScript, as the name implies, Subscript supports the everyday JavaScript that's just enough for most use-cases. That gives us something small and fast that fits anywhere. | ||
If we had this in real life in some sort of a UI render function… | ||
#### Transformable AST | ||
```js | ||
let count = 10, doubleCount, quadCount; | ||
``` | ||
Make any language transformation between `Subscript.parse(expr)` and `subscript.stringify()` / `subscript.eval()`. Subscript's syntax tree transformability offers a great way to do code transpiling, static code analysis, and more. | ||
```js | ||
let render = function() { | ||
let countElement = document.querySelector( '#count' ); | ||
countElement.innerHTML = count; | ||
doubleCount = count * 2; | ||
let doubleCountElement = document.querySelector( '#double-count' ); | ||
doubleCountElement.innerHTML = doubleCount; | ||
quadCount = doubleCount * 2; | ||
let quadCountElement = document.querySelector( '#quad-count' ); | ||
quadCountElement.innerHTML = quadCount; | ||
} | ||
``` | ||
#### A Pseudo Runtime! | ||
…then, we'd have to execute `render()` in whole each time the value of `count` changes. And here comes the additional overhead of querying the DOM every time! | ||
Subscript's `eval()` feature is a standalone JavaScript runtime that supports user-defined contexts. This provides the level of runtime encapsulation that is not available with the native JavaScript's `eval()` function. (*Examples ahead.*) | ||
In the time it takes to take a deep breath, we could make a drop-in replacement of the render function with a hypothetical function that, in addition to being a normal function, offers us a way to run expressions in isolation. | ||
#### Runtime Traps and Hooks | ||
```js | ||
render = new SubscriptFunction(` | ||
let countElement = document.querySelector( '#count' ); | ||
countElement.innerHTML = count; | ||
doubleCount = count * 2; | ||
let doubleCountElement = document.querySelector( '#double-count' ); | ||
doubleCountElement.innerHTML = doubleCount; | ||
quadCount = doubleCount * 2; | ||
let quadCountElement = document.querySelector( '#quad-count' ); | ||
quadCountElement.innerHTML = quadCount;` | ||
); | ||
``` | ||
Supercharge everything with runtime traps. Subscript accepts the same *trap* object as a Proxy trap, for intercepting runtime *assignment* `=`, `delete`, and `in` operators, and object accessors. This brings a new level of depth to JavaScript code. (*Examples ahead.*) | ||
\> Run as a normal function… | ||
### Examples | ||
```js | ||
render(); | ||
``` | ||
#### Evaluate a JavaScript expression | ||
The above executes the function body in full as designed… elements are selected and assigned content. And we can see the counters in the console. | ||
**`MathExpression`:** | ||
```js | ||
console,log( count, doubleCount, quadCount ); | ||
< 10, 20, 40 | ||
``` | ||
\> Run as a reactive function… | ||
```js | ||
var expr1 = '7 + 8'; | ||
var exprObj1 = Subscript.parse(expr1); | ||
// MathExpression | ||
count ++; | ||
render.signal( [ 'count' ] ); | ||
``` | ||
This time, only statements 2, 3, 5, 6, and 8 were run - *the count dependency thread*; and the previously selected UI elements in those local variables are updated. | ||
```js | ||
var result1 = exprObj1.eval(); | ||
// (Number) 15 | ||
console,log( count, doubleCount, quadCount ); | ||
< 11, 22, 44 | ||
``` | ||
**`ArrayExpression`:** | ||
Now, that's a bit of magic! But that hypothetical function is really Subscript Function! | ||
But before we go into the details, there's a fever pitch that can't wait: | ||
As trivial as our example code looks, we can see it applicable in real life places! Consider a neat reactive web component for our counter below. | ||
```js | ||
var expr2 = '[ "New York City", "Lagos", "Berlin" ]'; | ||
var exprObj2 = Subscript.parse(expr2); | ||
// ArrayExpression | ||
// We'll still keep count as a global variable for now | ||
let count = 10; | ||
``` | ||
```js | ||
var result2 = exprObj2.eval(); | ||
// (Array) [ "New York City", "Lagos", "Berlin" ] | ||
// This custom element extends Subscript as a base class… more on this later | ||
customElements.define( 'click-counter', class extends SubscriptElement( HTMLElement ) { | ||
// This is how we designate methods as reactive methods | ||
// But this is implicit having extended SubscriptElement() | ||
static get subscriptMethods() { | ||
return [ 'render' ]; | ||
} | ||
connectedCallback() { | ||
// Full execution at connected time | ||
this.render(); | ||
// Granularly-selective execution at click time | ||
this.addEventListener( 'click', () => { | ||
count ++; | ||
this.render.signal( [ 'count' ] ); | ||
} ); | ||
} | ||
render() { | ||
let countElement = document.querySelector( '#count' ); | ||
countElement.innerHTML = count; | ||
let doubleCount = count * 2; | ||
let doubleCountElement = document.querySelector( '#double-count' ); | ||
doubleCountElement.innerHTML = doubleCount; | ||
let quadCount = doubleCount * 2; | ||
let quadCountElement = document.querySelector( '#quad-count' ); | ||
quadCountElement.innerHTML = quadCount; | ||
} | ||
} ); | ||
``` | ||
**`ObjectExpression`:** | ||
## What Is Subscript? | ||
A general-purpose reactivity runtime for JavaScript, with an overarching philosophy of *reactivity that is based on the dependency graph of your own code, and nothing of its own syntax*! | ||
It takes any piece of code and compiles it into an ordinary JavaScript function that can also run expressions in dependency threads! | ||
Being function-based let's you have Subscript as a building block… to fit anywhere! | ||
+ [Concepts](#concepts) | ||
+ [API](#api) | ||
+ [Why Subscript](#why-subscript) | ||
## Concepts | ||
### Signals | ||
Subscript is not concerned with how changes happen or are detected on the outer scope of the function. It simply gives us a way to announce that something has changed. That announcement is called a *signal*. | ||
A Subscript function has a `signal()` method that lets us specify the list of outside variables or properties that have changed. | ||
```js | ||
var expr3 = '{ city1: "New York City", city2: "Lagos", city3: "Berlin", city4: cityNameFromContext }'; | ||
var exprObj3 = Subscript.parse(expr3); | ||
// ObjectExpression | ||
let a = 'Apple', b = 'Banana', c = { prop: 'Fruits' }; | ||
``` | ||
```js | ||
var result3 = exprObj3.eval(); | ||
// (Object) { city1: "New York City", city2: "Lagos", city3: "Berlin", city4: undefined } | ||
let fn = new SubscriptFunction(` | ||
console.log( \`The value of 'a' is: \${ a }\` ); | ||
console.log( \`The value of 'b' is: \${ b }\` ); | ||
console.log( \`The value of 'c.prop' is: \${ c.prop }\` ); | ||
`); | ||
``` | ||
```js | ||
var context = { cityNameFromContext: 'London' }; | ||
var result3 = exprObj3.eval(context); | ||
// (Object) { city1: "New York City", city2: "Lagos", city3: "Berlin", city4: "London" } | ||
// Initial run | ||
fn(); | ||
``` | ||
**`FunctionExpression`:** | ||
```js | ||
// Updates and signals | ||
b = 'Breadfruit'; | ||
fn.signal( [ 'b' ] ); | ||
``` | ||
The array syntax allows us to send signals for property changes as paths. | ||
```js | ||
var expr4 = '( arg1, arg2 ) => { return arg1 + arg2 + (argFromContext ? argFromContext : 0); }'; | ||
var exprObj4 = Subscript.parse(expr4); | ||
// FunctionExpression | ||
fn.signal( [ 'c', 'prop' ] ); | ||
``` | ||
And we can send multiple signals in one call. | ||
```js | ||
var result4 = exprObj4.eval(); | ||
// (Function) ( arg1, arg2 ) => { return arg1 + arg2 + (argFromContext ? argFromContext : 0); } | ||
fn.signal( [ 'a' ], [ 'b' ] ); | ||
``` | ||
result4(10, 3); | ||
// (Number) 13 | ||
Variables declared within the function belong in their own scope and do not respond to outside signals. But when they do reference variables from the outside scope, they are included in the dependency thread of those variables. | ||
```js | ||
let fn = new SubscriptFunction(` | ||
let a = 'Apple', b = 'Banana' + ' ' + c.prop; | ||
console.log( \`The value of 'a' is: \${ a }\` ); | ||
console.log( \`The value of 'b' is: \${ b }\` ); | ||
console.log( \`The value of 'c.prop' is: \${ c.prop }\` ); | ||
`); | ||
``` | ||
```js | ||
var context = { argFromContext: 20 }; | ||
var result4 = exprObj4.eval(context); | ||
// (Function) ( arg1, arg2 ) => { return arg1 + arg2 + (argFromContext ? argFromContext : 0); } | ||
// Initial run | ||
fn(); | ||
``` | ||
result4(10, 3); | ||
// (Number) 33 | ||
```js | ||
// The following signals will have no effect since a and b are local variables. | ||
fn.signal( [ 'a' ], [ 'b' ] ); | ||
``` | ||
**`.stringify()`:** | ||
```js | ||
// The local variable b will be part of the dependency thread for the following signal | ||
// (The console will therefore show the result of the last two statements in the function) | ||
fn.signal( [ 'c', 'prop' ] ); | ||
``` | ||
### References And Bindings | ||
Expressions and statements in Subscript maintain a binding to their references. And that's the basis for reactivity in Subscript. | ||
Variable declarations, with `let` and `var`, and assignment expressions, are bound to any references that may be in their argument. (`const` declarations are an exception as they're always *const* in nature.) | ||
```js | ||
var expr1 = exprObj1.stringify(); | ||
// 7 + 8 | ||
var tense = score > 50 ? 'passed' : 'failed'; | ||
``` | ||
#### Use conditionals, call a function in scope | ||
Above, `tense` is bound to the reference `score`. The effect of a signal from `score` is that `tense` is updated. That update, in turn, becomes a signal to any subsequent expression referencing `tense`. | ||
**`ConditionalExpression`:** | ||
```js | ||
let message = `Hi ${ candidate.firstName }, you ${ tense } this test!`; | ||
``` | ||
```js | ||
var expr1 = 'age < 18 ? fname + " " + lname + " does not meet the age requirement!" : fname + " " + lname + " is old enough!"'; | ||
var exprObj1 = Subscript.parse(expr1); | ||
// ConditionalExpression | ||
message += subjects.next ? ' Up next is: ' + subjects.next : ' The end!'; | ||
``` | ||
Above, `message` is bound to the references `candidate`, `candidate.firstName`, and `tense`. (Likewise, in the additional assignment for `message`, `message` is bound to the reference `subjects.next`.) The effect of a signal from any of these references is that `message` is updated. That update, in turn, becomes a signal to any subsequent expression referencing `message`. | ||
And the dependency thread continues! | ||
Other types of operations like `score ++`, and `delete subjects.next` make a signal to their dependents. | ||
Array/Object expressions, as another example, are bound to any references that they may be composed of, and the expressions are reevaluated should any of those change. | ||
```js | ||
var context = { fname: "John", lname: "Doe", age: 24 }; | ||
var result1 = exprObj1.eval(); | ||
// (String) John Doe is old enough! | ||
let fullName = [ candidate.firstName, candidate.lastName, ].join( ' ' ); | ||
``` | ||
Above, `fullName` is updated as any of `candidate`, `candidate.firstName`, `candidate.lastName` changes. | ||
```js | ||
var context = { fname: "John2", lname: "Doe2", age: 10 }; | ||
var result1 = exprObj1.eval(context); | ||
// (String) John Doe does not meet the age requirement! | ||
result = { …result, [ subjects.current ]: score }; | ||
``` | ||
**`StringExpression`:** | ||
Above, the `result` object gets, or updates, a property corresponding to `subjects.current`, with an associated value `score`, as any of `subjects.current` and `score` changes. | ||
References in call-time arguments are binding. | ||
```js | ||
var expr2 = '"Today is: " + date().stringify()'; | ||
var exprObj2 = Subscript.parse(expr2); | ||
// StringExpression | ||
alert( message ); // alert() runs again on receiving a change signal from message. | ||
``` | ||
```js | ||
var context = { date:() => (new Date) }; | ||
var result2 = exprObj2.eval(context); | ||
// (String) Today is: <current date> | ||
let candidate = new Candidate( id ); // candidate is a new instance on receiving a change signal from id. | ||
``` | ||
**`.stringify()`:** | ||
### Conditionals And Logic | ||
When the parameters of an *If/Else* statement, *Switch* statement, or other logical expressions contain references, the statement or logical expression is bound to those references. That lets us have reactive conditionals and logic. | ||
#### If/Else Statements | ||
When the *test* expression of an *If* statement contains references, the *if/else* construct is bound to those references. | ||
```js | ||
var expr2 = exprObj2.stringify(); | ||
// "Today is: " + date().stringify() | ||
if ( score > 80 && passesSomeOtherTest() ) { | ||
addBadge( candidate ); | ||
candidate.remark = 'You\'ve got a badge'; | ||
} else { | ||
} | ||
``` | ||
#### Write multiple expressions and Comments, plus a top-level return | ||
Above, the effect of a signal from the reference `score` is that the *test* expression (`score > 80 && passesSomeOtherTest()`) is evaluated again and the corresponding branch is executed in whole. | ||
**`BlockExpression`:** | ||
Now, adding an *else if* block would be just as adding another *if* statement in the *else* block of a parent *if* statement. | ||
```js | ||
var expr = ` | ||
/** | ||
* Block comments | ||
*/ | ||
if ( score > 80 && passesSomeOtherTest() ) { | ||
addBadge( candidate ); | ||
candidate.remark = 'You\'ve got a badge'; | ||
} else if ( someOtherCondition ) { | ||
} else { | ||
} | ||
``` | ||
// Single line comments | ||
delete obj1.prop1; | ||
delete /*comment anywhere*/obj2.prop1; | ||
Nested *If* statements have their own *if/else* branches bound to the references in their own *test* expression. So, above, every effect of a signal from the reference `someOtherCondition` is scoped to just the nested *if* statement. | ||
return; | ||
Now, for as long as one side of a logical expression remains active, expressions on the other side are always unexposed to signals. Above, for as long as the parent *test* expression (`score > 80 && passesSomeOtherTest()`) holds true: | ||
+ The *If* statement nested on its inactive side remains unexposed to signals from the reference `someOtherCondition`. | ||
+ The individual expressions nested on its active side remain exposed to signals from the references they themselves might be bound to. | ||
A change of state in the parent *test* expression reverses the situation. | ||
delete obj2.prop2; | ||
`; | ||
#### Switch Statements | ||
var exprObj = Subscript.parse(expr); | ||
// BlockExpression | ||
When the *switch* expression or any of the *test* expressions of a *Switch* statement contains references, the *Switch* construct is bound to those references. | ||
```js | ||
switch( score ) { | ||
case 0: | ||
candidate.remark = 'You got nothing at all'; | ||
break; | ||
case maxScore: | ||
candidate.remark = 'You got the most'; | ||
break; | ||
default: | ||
candidate.remark = defaultRemark; | ||
} | ||
``` | ||
Above, the effect of a signal on any of the references `score` and `maxScore` is that the *test* expressions are tested again and the body of the corresponding case is executed in whole. | ||
Now, for as long as the applicable case(s) of a *Switch* statement remain active, expressions in the others are always unexposed to signals. Above, for as long as the first or second case holds true, the assignment expression in the default case remains unexposed to signals from the reference `defaultRemark`. | ||
#### Logical And Ternary Operators | ||
Subscript observes the state of logical expressions (`a && b || c`), and conditional expressions with the *ternary* operator (`a ? b : c`), when running the dependency thread for a signal. | ||
```js | ||
var context = { obj1: { prop1: "John" }, obj2: { prop1: "Doe", prop2: "Bar" } }; | ||
var result = exprObj.eval(); | ||
let a = () => 1, b = 2, c = 3, d, e; | ||
``` | ||
context; | ||
// (Object) { obj1: { }, obj2: { prop2: "Bar" } } | ||
```js | ||
d = a() ? b : c; | ||
e = a() && b || c; | ||
``` | ||
#### Use traps | ||
Above, because of the *truthy* nature of the condition `a()`, the logical expressions will always return the value of `b`. This logic holds true even if the value of `c` continues to change. The assignment expressions are therefore not exposed at all to any signals from `c`, and this saves the potential overhead of calling `a()` each time `c` changes. | ||
**`IfExpression`:** | ||
Should the condition `a()` become *falsey*, as in `a = () => 0`, the scenario above changes to favour `c` over `b`. | ||
### Loops | ||
When the parameters of a loop (`for` loops, `do` and `do … while` loops) contain references, the loop is bound to those references. That lets us have reactive loops. | ||
#### A `for` Loop, `do` And `do … while` Loop | ||
When any of the three parts of a `for` loop contains references, the loop is bound to those references. | ||
```js | ||
var expr = `if ("prop1" in obj1) { | ||
console.log('prop1 exists.'); | ||
} else { | ||
console.log('prop1 does not exist. Creating it now.'); | ||
obj1.prop1 = 'John'; | ||
let start = 0, items = [ 'one', 'two', 'three', 'four', 'five' ], targetItems = []; | ||
``` | ||
```js | ||
for ( let index = start; index < items.length; index ++ ) { | ||
targetItems[ index ] = items[ index ]; | ||
} | ||
`; | ||
``` | ||
var exprObj = Subscript.parse(expr); | ||
// IfExpression | ||
Above, the effect of a signal from `start` or `items`, or `items.length`, is that the loop runs again. | ||
```js | ||
// Say, start and items were global variables | ||
start = 2; | ||
fn.signal( [ 'start' ] ); | ||
items.unshift( 'zero' ); | ||
fn.signal( [ 'items', 'length' ] ); | ||
``` | ||
Similar to a `for` loop, when the *condition* expression of a `do` or `do … while` loop contains references, the loop is bound to those references. | ||
```js | ||
var pseudoContext = { obj1: { prop1: "John" }, obj2: { prop1: "Doe", prop2: "Bar" } }; | ||
var context = {}; | ||
var result = exprObj.eval(context, { | ||
has: (target, property) => { | ||
return property in pseudoContext; | ||
}, | ||
set: (target, property, value) => { | ||
pseudoContext[property] = value; | ||
return true; | ||
}, | ||
}); | ||
let index = 0, items = [ 'one', 'two', 'three', 'four', 'five' ], targetItems = []; | ||
``` | ||
// (String) prop1 exists. | ||
```js | ||
while ( index < items.length ) { | ||
targetItems[ index ] = items[ index ]; | ||
index ++; | ||
} | ||
``` | ||
Above, the effect of a signal from `items`, or `items.length`, is that the loop runs again. | ||
```js | ||
// Say, items were global variables | ||
items.unshift( 'zero' ); | ||
fn.signal( [ 'items', 'length' ] ); | ||
``` | ||
#### A `for … of` Loop | ||
When the *iteratee* of a `for … of` loop is a reference, the loop is bound to that reference. | ||
```js | ||
let entries = [ 'one', 'two', 'three', 'four', 'five' ], targetEntries = []; | ||
``` | ||
```js | ||
for ( let entry of entries ) { | ||
let index = entries.indexOf( entry ); | ||
console.log( `Current iteration index is: ${ index }, and entry is: '${ entry }'` ); | ||
targetEntries[ index ] = entries[ index ]; | ||
} | ||
``` | ||
Above, the effect of a signal from the reference `entries` is that the loop runs again. | ||
```js | ||
// Say, entries were a global variable | ||
entries = [ 'six', 'seven', 'eight', 'nine', 'ten' ]; | ||
fn.signal( [ 'entries' ] ); | ||
``` | ||
As an added advantage of this form of loop, updating a specific entry in `entries` moves the loop's pointer to the specific iteration involving that entry, and the body of that iteration is run again. | ||
```js | ||
entries[ 7 ] = 'This is new eight'; | ||
fn.signal( [ 'entries', 7 ] ); | ||
``` | ||
Now, the console reports… | ||
```js | ||
Current iteration index is: 7, and entry is: 'This is new eight' | ||
``` | ||
…and index `7` of `targetEntries` is updated. | ||
#### A `for … in` Loop | ||
When the *iteratee* of a `for … in` loop is a reference, the loop is bound to that reference. | ||
```js | ||
let entries = { one: 'one', two: 'two', three: 'three', four: 'four', five: 'five' }, targetEntries = {}; | ||
``` | ||
```js | ||
for ( let propertyName in entries ) { | ||
console.log( `Current property name is: ${ propertyName }, and associated value is: '${ entries[ propertyName ] }'` ); | ||
targetEntries[ propertyName ] = entries[ propertyName ]; | ||
} | ||
``` | ||
Above, the effect of a signal from the reference `entries` is that the loop runs again. | ||
```js | ||
// Say, entries were a global variable | ||
entries = { six: 'six', seven: 'seven', eight: 'eight', nine: 'nine', ten: 'ten' }; | ||
fn.signal( [ 'entries' ] ); | ||
``` | ||
As an added advantage of this form of loop, updating a specific property in `entries` moves the loop's pointer to the specific iteration involving that property, and the body of that iteration is run again. | ||
```js | ||
entries[ 'eight' ] = 'This is new eight'; | ||
fn.signal( [ 'entries', 'eight' ] ); | ||
``` | ||
Now, the console reports… | ||
```js | ||
Current property name is: eight, and property value is: 'This is new eight' | ||
``` | ||
…and the property `eight` of `targetEntries` is updated. | ||
#### Iteration States | ||
Conceptually, each round of iteration in a loop is an instance that Subscript can access directly during a reactive run. A round of iteration is thus updatable in isolation in response to a directed signal. This is what happens when the *iteratee* of a `for … of` or `for … in` loop *signals* about an updated entry, as seen above. | ||
Below is a similar case. | ||
```js | ||
let entries = { one: { name: 'one' }, two: { name: 'two' } }, targetEntries = {}; | ||
``` | ||
```js | ||
for ( let propertyName in entries ) { | ||
console.log( `Current property name is: ${ propertyName }, and its alias name is: '${ entries[ propertyName ].name }'` ); | ||
targetEntries[ propertyName ] = entries[ propertyName ]; | ||
} | ||
``` | ||
On updating the first entry, only the first round of iteration is executed again. | ||
```js | ||
entries[ 'one' ] = { name: 'New one' }; | ||
fn.signal( [ 'entries', 'one' ] ); | ||
``` | ||
For even more granularity, individual expressions inside a round of iteration are also responsive to signals of their own. So, if we updated just `entries.one.name`… | ||
```js | ||
entries.one.name = 'New one'; | ||
fn.signal( [ 'entries', 'one', 'name' ] ); | ||
``` | ||
…we would have skipped the iteration instance itself, to match just the first statement within it. | ||
This granular reactivity makes it often pointless to trigger a full rerun of a loop, offering multiple opportunities to deliver unmatched performance. | ||
#### Breakouts | ||
Subscript observes `break` and `continue` statements even in a reactive run. And any of these statements may employ *labels*. | ||
```js | ||
let entries = { one: { name: 'one' }, two: { name: 'two' } }; | ||
``` | ||
```js | ||
parentLoop: for ( let propertyName in entries ) { | ||
childLoop: for ( let subPropertyName in entries[ propertyName ] ) { | ||
If ( propertyName === 'one' ) { | ||
break parentLoop; | ||
} | ||
console.log( propertyName, subPropertyName ); | ||
} | ||
} | ||
``` | ||
### Functions | ||
## Documentation | ||
@@ -240,0 +542,0 @@ |
@@ -5,3 +5,3 @@ | ||
*/ | ||
import Subscript from './index.js'; | ||
import Subscript from './Subscript.js'; | ||
@@ -8,0 +8,0 @@ // As globals |
@@ -5,19 +5,10 @@ | ||
*/ | ||
import Parser from './Parser.js'; | ||
import grammar from './grammar.js'; | ||
import Runtime from './Runtime.js'; | ||
import Scope from './Scope.js'; | ||
import Compiler from './compiler/Compiler.js'; | ||
import Runtime from './runtime/Runtime.js'; | ||
import Subscript from './Subscript.js'; | ||
/** | ||
* @var object | ||
*/ | ||
Parser.grammar = grammar; | ||
/** | ||
* @exports | ||
*/ | ||
export { | ||
Parser, | ||
Runtime, | ||
Scope, | ||
Subscript as default, | ||
Compiler, | ||
Runtime, | ||
} |
@@ -1,47 +0,25 @@ | ||
/** | ||
* @imports | ||
*/ | ||
import _isString from '@webqit/util/js/isString.js'; | ||
import CallInterface from './grammar/CallInterface.js'; | ||
import NumInterface from './grammar/NumInterface.js'; | ||
import StrInterface from './grammar/StrInterface.js'; | ||
/** | ||
* UTILS | ||
* @normalizeTabs | ||
*/ | ||
export function referencesToPaths(references) { | ||
return references.map(expr => { | ||
var seg = expr, pathArray = []; | ||
pathArray.dotSafe = true; | ||
do { | ||
if (seg instanceof CallInterface) { | ||
pathArray.splice(0); | ||
seg = seg.reference; | ||
} | ||
if (_isString(seg.name)) { | ||
pathArray.unshift(seg.name); | ||
pathArray.dotSafe = pathArray.dotSafe && !seg.name.includes('.'); | ||
} else if (seg.name instanceof NumInterface) { | ||
pathArray.unshift(seg.name.int); | ||
pathArray.dotSafe = pathArray.dotSafe && !(seg.name.int + '').includes('.'); | ||
} else if (seg.name instanceof StrInterface) { | ||
pathArray.unshift(seg.name.expr); | ||
pathArray.dotSafe = pathArray.dotSafe && !seg.name.expr.includes('.'); | ||
} else { | ||
pathArray.splice(0); | ||
} | ||
} while(seg = seg.context); | ||
if (pathArray.dotSafe) { | ||
return (new DotSafePath).concat(pathArray); | ||
export function normalizeTabs( rawSource ) { | ||
let rawSourceSplit = rawSource.split(/\n/g); | ||
if ( rawSourceSplit.length > 1 ) { | ||
let possibleBodyIndentLevel = rawSourceSplit[ 1 ].split(/[^\s]/)[0].length; | ||
if ( possibleBodyIndentLevel ) { | ||
return rawSourceSplit.map( ( line, i ) => { | ||
if ( !i ) return line; | ||
let possibleIndent = line.substring( 0, possibleBodyIndentLevel ); | ||
if ( !possibleIndent.trim().length ) { | ||
return line.substring( possibleBodyIndentLevel ); | ||
} | ||
// Last line? | ||
if ( possibleIndent.trim() === '}' && i === rawSourceSplit.length - 1 ) { | ||
return '}'; | ||
} | ||
return line; | ||
} ).join( "\n" ); | ||
} | ||
return pathArray; | ||
}); | ||
} | ||
export class DotSafePath extends Array { | ||
static resolve(path) { | ||
// Note the concat() below... | ||
// the spread operator: new DotSafePath(...path) doesn't work when path is [0]. | ||
return path.every(v => !(v + '').includes('.')) ? (new DotSafePath).concat(path) : path; | ||
} | ||
get dotSafe() { return true } | ||
} | ||
return rawSource; | ||
} |
@@ -1,116 +0,115 @@ | ||
/** | ||
* @imports | ||
*/ | ||
import { expect } from 'chai'; | ||
import { Parser } from '../src/index.js'; | ||
import { Block } from '../src/grammar.js'; | ||
import { group, add } from './driver.js'; | ||
describe(`Test: AST`, function() { | ||
/** | ||
* Note that the format of the code block in the "expected" argument | ||
* is: 4 spaces as indentation. The AST compiler has been configured for same 4 spaces as indentation. | ||
* But "startingIndentLevel" can be anything as this is automatically detected and applied accordingly | ||
* on the AST compiler. Also, both strings are ".trim()" before comparison. | ||
* @see ./driver.js | ||
*/ | ||
describe(`Object syntax`, function() { | ||
describe(`Variable declarations`, function() { | ||
it(`Should parse simple object expression: { prop1: value1, prop2: "String value2", prop3: 3 }`, function() { | ||
var expr = `{ prop1: value1, prop2: "String value2", prop3: 3 }`; | ||
var AST = Parser.parse(expr); | ||
var VAL = AST.eval(); | ||
expect(AST + '').to.eq(expr); | ||
expect(VAL).to.eql({ prop1: undefined, prop2: 'String value2', prop3: 3 }); | ||
}); | ||
group(`Maintain the declaration of variables that have "unobservable" initializers.`, function() { | ||
it(`Should parse dynamic object expression: { [prop1]: value1, "prop 2": "String value2", prop3, [prop4], 'ddd': 'ddd' }`, function() { | ||
var expr = `{ [prop1]: value1, "prop 2": "String value2", prop3, [prop4], 'ddd': 'ddd' }`; | ||
var AST = Parser.parse(expr); | ||
var VAL = AST.eval({ prop4: 'dynamicProp' }); | ||
expect(AST + '').to.eq(expr); | ||
expect(VAL).to.eql({ undefined: undefined, 'prop 2': 'String value2', prop3: undefined, dynamicProp: 'dynamicProp', ddd: 'ddd' }); | ||
}); | ||
add( | ||
`Ordinary "Identifier" initializers (e.g var a = b;) are not "observable".`, | ||
` | ||
var a = b; | ||
`, | ||
` | ||
var a = b; | ||
` | ||
); | ||
add( | ||
`"const" kind of declarations (e.g const a = b.c;) are not "observable".`, | ||
` | ||
const a = b.c; | ||
`, | ||
` | ||
const a = b.c; | ||
` | ||
); | ||
}); | ||
describe(`Abstraction () syntax`, function() { | ||
group(`Refactor and observe around the declaration of variables that have "observable" initializers.`, function() { | ||
it(`Should parse simple abstraction expression: (4 + 10)`, function() { | ||
var expr = `(4 + 10)`; | ||
var AST = Parser.parse(expr); | ||
var VAL = AST.eval(); | ||
expect(AST + '').to.eq(expr); | ||
expect(VAL).to.eql(14); | ||
}); | ||
add( | ||
`"MemberExpression" initializers (e.g var a = b.c;) are "observable".`, | ||
` | ||
var a = b.c; | ||
`, | ||
` | ||
var a; | ||
$("var#1", [["b", "c"]], () => { | ||
a = b.c; | ||
}); | ||
` | ||
); | ||
it(`Should parse multiple statement abstraction expression: (4 + 10, 555 + 1)`, function() { | ||
var expr = `(4 + 10, 555 + 1)`; | ||
var AST = Parser.parse(expr); | ||
var VAL = AST.eval(); | ||
expect(AST + '').to.eq(expr); | ||
expect(VAL).to.eql(556); | ||
}); | ||
add( | ||
`In a multi-var declaration (e.g let a = b, c = d.e, f = g;), observable declarations are singled out into a new line for observability.`, | ||
` | ||
let a = b, c = d.e, f = g; | ||
`, | ||
` | ||
let a = b, c, f = g; | ||
$("let#1", [["d", "e"]], () => { | ||
c = d.e; | ||
}); | ||
` | ||
); | ||
}); | ||
describe(`Assignment syntax`, function() { | ||
}); | ||
it(`Should parse simple assignment expression: aa = 4 + 10`, function() { | ||
var expr = `aa = 4 + 10`; | ||
var AST = Parser.parse(expr); | ||
var CONTEXT = { aa: 0 }; | ||
var VAL = AST.eval(CONTEXT, {strictMode: false}); | ||
expect(AST + '').to.eq(expr); | ||
expect(VAL).to.eq(14); | ||
expect(CONTEXT).to.eql({ aa: 14 }); | ||
}); | ||
describe(`Assignment expressions`, function() { | ||
it(`Should parse path assignment expression: bb.rr = 4 + 10`, function() { | ||
var expr = `bb.rr = 4 + 10`; | ||
var AST = Parser.parse(expr); | ||
var CONTEXT = { bb: {} }; | ||
var VAL = AST.eval(CONTEXT); | ||
expect(AST + '').to.eq(expr); | ||
expect(VAL).to.eq(14); | ||
expect(CONTEXT).to.eql({ bb: { rr: 14 } }); | ||
}); | ||
group(`Maintain assignment expressions that have "unobservable" right-hand side.`, function() { | ||
add( | ||
`Ordinary "Identifier" right-hand sides (e.g a = b;) are not "observable".`, | ||
` | ||
a = b; | ||
`, | ||
` | ||
a = b; | ||
` | ||
); | ||
}); | ||
describe(`Variable decalarations syntax`, function() { | ||
group(`Refactor and observe around assignment expressions that have "observable" right-hand side.`, function() { | ||
it(`Should parse simple variable decalaration: var aa = 4 + 10`, function() { | ||
var expr = `var aa = 4 + 10`; | ||
var AST = Parser.parse(expr); | ||
var SCOPEOBJ = {}; | ||
var CONTEXT = {}; | ||
var VAL = AST.eval(CONTEXT, { scopeObj: SCOPEOBJ }); | ||
expect(SCOPEOBJ.local).to.eql({ aa: 14 }); | ||
}); | ||
add( | ||
`"MemberExpression" right-hand sides (e.g a = b.c;) are "observable".`, | ||
` | ||
a = b.c; | ||
`, | ||
` | ||
$("assign-global#1", [["b", "c"]], () => { | ||
a = b.c; | ||
}); | ||
` | ||
); | ||
it(`Should parse multiple variable decalarations: var aa = 4 + 10, bb = 2, cc, dd = 3, ee`, function() { | ||
var expr = `var aa = 4 + 10, bb = 2, cc, dd = 3, ee`; | ||
var AST = Parser.parse(expr); | ||
var SCOPEOBJ = {}; | ||
var CONTEXT = {}; | ||
var VAL = AST.eval(CONTEXT, { scopeObj: SCOPEOBJ }); | ||
expect(SCOPEOBJ.local).to.eql({ aa: 14, bb: 2, cc: undefined, dd: 3, ee: undefined }); | ||
}); | ||
}); | ||
add( | ||
`In a sequence expression (e.g a = b, c = d.e, f = g;), assignment expressions with observable right-hand side are singled out into a new line for observability.`, | ||
` | ||
a = b, c = d.e, f = g; | ||
`, | ||
` | ||
(a = b, $("assign-global#1", [["d", "e"]], () => c = d.e), f = g); | ||
` | ||
); | ||
describe(`Statement block syntax`, function() { | ||
it(`Should parse simple statement block: var aa = 4 + 10; console.log(aa);`, function() { | ||
var expr = `var aa = 4 + 10; console.log(aa);`; | ||
var AST = Parser.parse(expr, [ Block ]); | ||
var SCOPEOBJ = {}; | ||
var CONTEXT = {}; | ||
var VAL = AST.eval(CONTEXT, { scopeObj: SCOPEOBJ }); | ||
expect(SCOPEOBJ.local).to.eql({ aa: 14 }); | ||
}); | ||
it(`Should parse complex statement block: var aa = 4 + 10, bb = 2, cc, dd = 3, ee = (b, 20); if (w) {} console.log(aa), console.log(bb);`, function() { | ||
var expr = `var aa = 4 + 10, bb = 2, cc, dd = 3, ee = (b, 20); if (w) {} console.log(aa)\r\n console.log(bb);`; | ||
var AST = Parser.parse(expr, [ Block ]); | ||
var SCOPEOBJ = {}; | ||
var CONTEXT = {}; | ||
var VAL = AST.eval(CONTEXT, { scopeObj: SCOPEOBJ }); | ||
expect(SCOPEOBJ.local).to.eql({ aa: 14, bb: 2, cc: undefined, dd: 3, ee: 20 }); | ||
}); | ||
}); | ||
}); |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
4239809
6495
551
2
7
66
1
+ Addedacorn@^8.7.0
+ Addedastring@^1.8.1
+ Addedacorn@8.14.0(transitive)
+ Addedastring@1.9.0(transitive)
- Removed@webqit/util@^0.8.8
- Removed@webqit/util@0.8.14(transitive)