Skip to content

Commit

Permalink
refactor!: change lexer to use acorn and escodegen
Browse files Browse the repository at this point in the history
- refactor!: change lexer to use acorn and codegen instead of js-tokens.
Realized that creating our own AST reader is a huge task, make take
a lot of time and prone for errors if we want to be spec compliant.
With this, I decided to use acorn and codegen instead in terms of
identifying variables.
- fix: non-MiniJS variables are being read by Mini.js.
I made so that we are not automatically reading user
defined global variables. We now only fetch variables from
attributes and events. Currently, we can't fetch from
the scripts since we are not sure whether that script is
defined by a user or a library.
- fix: not being able to declare variables due to being read
as a proxy variable.
  • Loading branch information
jorenrui committed Nov 22, 2023
1 parent d916721 commit ef03957
Show file tree
Hide file tree
Showing 6 changed files with 366 additions and 371 deletions.
3 changes: 0 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/base16/google-light.min.css">

<script>hljs.highlightAll();</script>

<!-- TODO: Ignore non-minijs variables -->
<!-- <script>MiniJS.ignore = ['MiniJS', 'tailwind', 'hljs', 'proxyWindow', '/template.html', 'StyleSheetApplicableStateChangeEvent', 'XULElement', 'StyleSheetRemovedEvent']</script> -->
</head>

<body class="pb-64">
Expand Down
230 changes: 86 additions & 144 deletions lib/entity.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Lexer from './lexer'
import Lexer from './lexer';

export default class Entity {
constructor(el) {
Expand All @@ -9,8 +9,7 @@ export default class Entity {
}
this.variables = []
this.dynamicAttributes = [];
this.dependencies = [];
this.id = this.generateEntityUUID()
this.id = this.generateEntityUUID();

this._getDynamicAttributes();
}
Expand All @@ -25,132 +24,91 @@ export default class Entity {
}

_getDynamicAttributes() {
const IGNORED_ATTRIBUTES = [':class', ':text', ':value'];
const RESERVED_KEYWORDS = ['document', 'window', 'proxyWindow'];

const attributes = Array.from(this.element.attributes)
const dynamicAttributes = attributes.filter((attr) => (
!IGNORED_ATTRIBUTES.includes(attr.name)
&& attr.name.startsWith(':')
&& this.element.hasAttribute(attr.name.slice(1))
));

this.dynamicAttributes = dynamicAttributes.map((attr) => {
const lexer = new Lexer(attr.value);

lexer.filter(Lexer.TOKEN.IdentifierName).forEach((token) => {
if (RESERVED_KEYWORDS.includes(token.value)) return;
if (token.value === 'el') return;
if (token.declaration || token.assignment || token.accessed || token.method || token.calculated) return;
for (let i = 0; i < this.element.attributes.length; i++) {
const attr = this.element.attributes[i];
if (MiniJS.allCustomBindings.includes(attr.name)) continue;
if (MiniJS.allEvents.includes(attr.name) || this.allEvents.includes(attr.name)) continue;
if (!attr.name.startsWith(':')) continue;
if (this.dynamicAttributes.includes(attr.name)) continue;
this.dynamicAttributes.push(attr.name);
}
}

const parentTokens = token.parent?.split('.');
if (RESERVED_KEYWORDS.includes(parentTokens?.[0])) return;
getVariables() {
this._getVariablesFromAttributes();
this._getVariablesFromEvents();
this._initVariables();
}

_getVariablesFromAttributes() {
const RESERVED_KEYWORDS = ['$', 'window', 'document', 'console'];
const CUSTOM_ATTRIBUTES = [':each', ':class', ':text', ':value', ':checked'];

[...this.dynamicAttributes, ...CUSTOM_ATTRIBUTES].forEach((name) => {
const attr = this.element.attributes[name];
if (!attr) return;

// Ignore object with no assignment
if (token.parent?.length === 0) return;
const lexer = new Lexer(attr.value, RESERVED_KEYWORDS);
const { referenced, member, assigned } = lexer.identifiers;

const value = token.parent
? `${token.parent}.${token.value}`
: token.value;
const filtered = [...referenced, ...member, ...assigned].filter((value) => {
const isNativeVariable = typeof(window[value]) === "function"
&& window[value].toString().indexOf("[native code]") === -1;

if (this.dependencies.includes(value)) return;
this.dependencies.push(value);
});
return !isNativeVariable;
})

this.variables.push(...filtered);

return attr.name;
});

this.dynamicAttributes = [...new Set(this.dynamicAttributes)];
this.dependencies = [...new Set(this.dependencies)];

this._initDependencies();
}

_initDependencies() {
this.dependencies.forEach((value) => {
if (value.startsWith('el.')) {
this.setAsParent();
_getVariablesFromEvents() {
const RESERVED_KEYWORDS = ['event', '$', 'window', 'document', 'console'];

const varName = value.replace("el.", "");
this.allEvents.forEach((event) => {
const expr = this.element.getAttribute(event);

const lexer = new Lexer(expr, RESERVED_KEYWORDS);
const { referenced, member, assigned } = lexer.identifiers;

if (!window[this.uuid])
window[this.uuid] = {};
window[this.uuid][varName] = MiniJS.tryFromLocal(value.replace("el.", this.uuid))
const filtered = [...referenced, ...member, ...assigned].filter((value) => {
const isNativeVariable = typeof(window[value]) === "function"
&& window[value].toString().indexOf("[native code]") === -1;

return !isNativeVariable;
});

if (!this.dependencies.includes(this.uuid))
this.dependencies.push(this.uuid);
} else {
window[value] = MiniJS.tryFromLocal(value);
}
this.variables.push(...filtered);
});
}

getVariablesFromEvents() {
const RESERVED_KEYWORDS = ['event', 'document', 'window', 'this', 'proxyWindow', '$', 'Date', 'proxyWindow', 'MiniJS'];
_initVariables() {
this.variables = [...new Set(this.variables)];
MiniJS.variables = [...new Set(MiniJS.variables.concat(this.variables))];

this.allEvents.forEach(event => {
const expr = this.element.getAttribute(event);
const lexer = new Lexer(expr);
const tokens = [];

lexer.filter(Lexer.TOKEN.IdentifierName).forEach((token) => {
if (RESERVED_KEYWORDS.includes(token.value)) return;
if (token.value === 'el') return;
if (token.declaration || token.accessed || token.method || token.calculated) return;
this.variables.forEach((variable) => {
if (variable.startsWith('el.')) {
this.setAsParent();

const parentTokens = token.parent?.split('.');
if (RESERVED_KEYWORDS.includes(parentTokens?.[0])) return;
if (!this.parent)
this.parent = this.getParent();

// Ignore object with no assignment
if (token.parent?.length === 0) return;
const varName = variable.replace("el.", "");

const value = token.parent
? `${token.parent}.${token.value}`
: token.value;

if (tokens.includes(value)) return;
if (value.startsWith('el.')) {
this.setAsParent();

const varName = value.replace("el.", "");

if (!window[this.uuid])
window[this.uuid] = {};
window[this.uuid][varName] = MiniJS.tryFromLocal(value.replace("el.", this.uuid))

if (!tokens.includes(this.uuid))
tokens.push(this.uuid);
} else {
window[value] = value.startsWith('$')
? MiniJS.tryFromLocal(value)
: window[value] ?? undefined;
}

tokens.push(value);
});

MiniJS.variables.push(...tokens);
});

MiniJS.variables = [...new Set(MiniJS.variables)];
}
if (!window[this.uuid])
window[this.uuid] = {};
window[this.uuid][varName] = MiniJS.tryFromLocal(variable.replace("el.", this.uuid))

getVariables() {
const allVariables = MiniJS.variables
const attributeToken = Array.from(this.element.attributes).map(attr => attr.value)
const filtered = [...new Set(allVariables.filter(v => attributeToken.find(t => t.includes(v))))]

for (let token of filtered) {
if (typeof window[token] === 'function') {
const otherVariables = allVariables.filter(v => window[token].toString().includes(v))
filtered.concat(otherVariables)
}
if (token.includes("el.") && !this.parent) {
this.parent = this.getParent()
if (!this.variables.includes(this.uuid))
this.variables.push(this.uuid);
} else {
window[variable] = variable.startsWith('$')
? MiniJS.tryFromLocal(variable)
: window[variable];
}
}

this.variables = filtered
});
}

get allEvents() {
Expand Down Expand Up @@ -180,52 +138,37 @@ export default class Entity {

_sanitizeExpression(expr) {
const lexer = new Lexer(expr);
const ids = {
'$': 'document.querySelector',
'this': 'this.element',
};

lexer.replace((token) => {
if (token.type === Lexer.TOKEN.ReservedWord && token.value === 'this'
&& token.parent == null && !token.declaration) {
return 'this.element';
}
});

lexer.replace((token) => {
if (token.type === Lexer.TOKEN.IdentifierName && token.value === '$'
&& token.parent == null && !token.declaration && !token.assignment) {
return 'document.querySelector';
}
this.variables.forEach((variable) => {
ids[variable] = `proxyWindow.${variable}`;
});

if (this.parent)
lexer.replace((token) => {
if (token.type === Lexer.TOKEN.IdentifierName && token.value === 'el'
&& token.parent == null && !token.declaration) {
return `proxyWindow['${this.parent.uuid}']`;
}
});

this.variables.forEach((variable) => {
lexer.replace((token) => {
if (token.type === Lexer.TOKEN.IdentifierName && token.value === variable
&& token.parent == null && !token.declaration) {
return `proxyWindow.${variable}`;
}
});
})
ids.el = `proxyWindow['${this.parent.uuid}']`;

lexer.replace(ids, ['declared']);

return lexer.output();
}

_sanitizeContentExpression(expr) {
if (expr.includes("el.")) {
let parentEntity = this.parent
this.variables.forEach(variable => {
if (variable.includes("el.")) {
const newExpr = `proxyWindow.${parentEntity.uuid}['${variable.replace("el.", "")}']`
expr = expr.replace(variable, newExpr)
}
})
const lexer = new Lexer(expr);

lexer.replace({
'$': 'document.querySelector',
'el': `proxyWindow['${this.parent.uuid}']`,
'this': 'this.element',
}, ['declared']);

return lexer.output();
}
return expr

return expr;
}

getParent() {
Expand Down Expand Up @@ -291,7 +234,6 @@ export default class Entity {

for (let i = 0; i < elements.length; i++) {
const entity = new Entity(elements[i])
entity.getVariablesFromEvents()
entity.getVariables()
entity.applyEventBindings()
entity.evaluateAll()
Expand Down
Loading

0 comments on commit ef03957

Please sign in to comment.