mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 66a801393f | |||
| e7f405cc97 | |||
| 55c48b2b12 | |||
| 2eb151c92d | |||
| 7952f31e63 | |||
| 11e4e67599 | |||
| 7b7a6de373 | |||
| b63d1e28b2 | |||
| c0667a11c6 | |||
| fddb1ec924 | |||
| e6c3b62ef0 | |||
| 97b69f164f | |||
| 33dfeb1b41 | |||
| dd292472b9 | |||
| 9b18b57fdf | |||
| 68f491cd32 | |||
| 7b3e39ba27 |
@@ -47,3 +47,4 @@ Utility/helpers:
|
|||||||
- [`status`](reference/component.md#status-helper): utility function to get the status of a component (new, mounted or destroyed)
|
- [`status`](reference/component.md#status-helper): utility function to get the status of a component (new, mounted or destroyed)
|
||||||
- [`validate`](reference/utils.md#validate): validates if an object satisfies a specified schema
|
- [`validate`](reference/utils.md#validate): validates if an object satisfies a specified schema
|
||||||
- [`whenReady`](reference/utils.md#whenready): utility function to execute code when DOM is ready
|
- [`whenReady`](reference/utils.md#whenready): utility function to execute code when DOM is ready
|
||||||
|
- [`batched`](reference/utils.md#batched): utility function to batch function calls
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ class ComponentB extends owl.Component {
|
|||||||
count: {type: Number},
|
count: {type: Number},
|
||||||
messages: {
|
messages: {
|
||||||
type: Array,
|
type: Array,
|
||||||
element: {type: Object, shape: {id: Boolean, text: String }
|
element: {type: Object, shape: {id: Boolean, text: String }}
|
||||||
},
|
},
|
||||||
date: Date,
|
date: Date,
|
||||||
combinedVal: [Number, Boolean],
|
combinedVal: [Number, Boolean],
|
||||||
@@ -276,7 +276,8 @@ class ComponentB extends owl.Component {
|
|||||||
id: Number,
|
id: Number,
|
||||||
name: {type: String, optional: true},
|
name: {type: String, optional: true},
|
||||||
url: String
|
url: String
|
||||||
]}, // object, with keys id (number), name (string, optional) and url (string)
|
}
|
||||||
|
}, // object, with keys id (number), name (string, optional) and url (string)
|
||||||
someObj3: {
|
someObj3: {
|
||||||
type: Object,
|
type: Object,
|
||||||
values: { type: Array, element: String },
|
values: { type: Array, element: String },
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ This may seem counter-intuitive, but it makes perfect sense in the context of co
|
|||||||
```js
|
```js
|
||||||
class DoubleCounter extends Component {
|
class DoubleCounter extends Component {
|
||||||
static template = xml`
|
static template = xml`
|
||||||
<t t-esc="state.selected + ': ' + state[state.selected].value"/>
|
<t t-esc="'selected: ' + state.selected + ', value: ' + state[state.selected]"/>
|
||||||
<button t-on-click="() => this.state.count1++">increment count 1</button>
|
<button t-on-click="() => this.state.count1++">increment count 1</button>
|
||||||
<button t-on-click="() => this.state.count2++">increment count 2</button>
|
<button t-on-click="() => this.state.count2++">increment count 2</button>
|
||||||
<button t-on-click="changeCounter">Switch counter</button>
|
<button t-on-click="changeCounter">Switch counter</button>
|
||||||
@@ -193,7 +193,7 @@ to be able to opt out of creating them in the first place. This is the purpose o
|
|||||||
### `markRaw`
|
### `markRaw`
|
||||||
|
|
||||||
Marks an object so that it is ignored by the reactivity system, meaning that if this object is ever
|
Marks an object so that it is ignored by the reactivity system, meaning that if this object is ever
|
||||||
part of a of a reactive object, it will be returned as is, and no keys in that object will be
|
part of a reactive object, it will be returned as is, and no keys in that object will be
|
||||||
observed.
|
observed.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ Slots can define a default content, in case the parent did not define them:
|
|||||||
## Dynamic Slots
|
## Dynamic Slots
|
||||||
|
|
||||||
The `t-slot` directive is actually able to use any expressions, using string
|
The `t-slot` directive is actually able to use any expressions, using string
|
||||||
interplolation:
|
interpolation:
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<t t-slot="{{current}}" />
|
<t t-slot="{{current}}" />
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ functions are all available in the `owl.utils` namespace.
|
|||||||
- [`loadFile`](#loadfile): loading a file (useful for templates)
|
- [`loadFile`](#loadfile): loading a file (useful for templates)
|
||||||
- [`EventBus`](#eventbus): a simple EventBus
|
- [`EventBus`](#eventbus): a simple EventBus
|
||||||
- [`validate`](#validate): a validation function
|
- [`validate`](#validate): a validation function
|
||||||
|
- [`batched`](#batched): batch function calls
|
||||||
|
|
||||||
## `whenReady`
|
## `whenReady`
|
||||||
|
|
||||||
@@ -78,3 +79,22 @@ validate(
|
|||||||
// - 'id' is missing (should be a number),
|
// - 'id' is missing (should be a number),
|
||||||
// - 'url' is missing (should be a boolean or list of numbers),
|
// - 'url' is missing (should be a boolean or list of numbers),
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `batched`
|
||||||
|
|
||||||
|
The `batched` function creates a batched version of a callback so that multiple calls to it within the same microtick will only result in a single invocation of the original callback.
|
||||||
|
|
||||||
|
```js
|
||||||
|
function hello() {
|
||||||
|
console.log("hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
const batchedHello = batched(hello);
|
||||||
|
batchedHello();
|
||||||
|
// Nothing is logged
|
||||||
|
batchedHello();
|
||||||
|
// Still not logged
|
||||||
|
|
||||||
|
await Promise.resolve(); // Await the next microtick
|
||||||
|
// "hello" is logged only once
|
||||||
|
```
|
||||||
|
|||||||
+39
-30
@@ -1850,8 +1850,9 @@ const NO_CALLBACK = () => {
|
|||||||
};
|
};
|
||||||
const objectToString = Object.prototype.toString;
|
const objectToString = Object.prototype.toString;
|
||||||
const objectHasOwnProperty = Object.prototype.hasOwnProperty;
|
const objectHasOwnProperty = Object.prototype.hasOwnProperty;
|
||||||
const SUPPORTED_RAW_TYPES = new Set(["Object", "Array", "Set", "Map", "WeakMap"]);
|
// Use arrays because Array.includes is faster than Set.has for small arrays
|
||||||
const COLLECTION_RAWTYPES = new Set(["Set", "Map", "WeakMap"]);
|
const SUPPORTED_RAW_TYPES = ["Object", "Array", "Set", "Map", "WeakMap"];
|
||||||
|
const COLLECTION_RAW_TYPES = ["Set", "Map", "WeakMap"];
|
||||||
/**
|
/**
|
||||||
* extract "RawType" from strings like "[object RawType]" => this lets us ignore
|
* extract "RawType" from strings like "[object RawType]" => this lets us ignore
|
||||||
* many native objects such as Promise (whose toString is [object Promise])
|
* many native objects such as Promise (whose toString is [object Promise])
|
||||||
@@ -1874,7 +1875,7 @@ function canBeMadeReactive(value) {
|
|||||||
if (typeof value !== "object") {
|
if (typeof value !== "object") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return SUPPORTED_RAW_TYPES.has(rawType(value));
|
return SUPPORTED_RAW_TYPES.includes(rawType(value));
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Creates a reactive from the given object/callback if possible and returns it,
|
* Creates a reactive from the given object/callback if possible and returns it,
|
||||||
@@ -2044,7 +2045,7 @@ function reactive(target, callback = NO_CALLBACK) {
|
|||||||
const reactivesForTarget = reactiveCache.get(target);
|
const reactivesForTarget = reactiveCache.get(target);
|
||||||
if (!reactivesForTarget.has(callback)) {
|
if (!reactivesForTarget.has(callback)) {
|
||||||
const targetRawType = rawType(target);
|
const targetRawType = rawType(target);
|
||||||
const handler = COLLECTION_RAWTYPES.has(targetRawType)
|
const handler = COLLECTION_RAW_TYPES.includes(targetRawType)
|
||||||
? collectionsProxyHandler(target, callback, targetRawType)
|
? collectionsProxyHandler(target, callback, targetRawType)
|
||||||
: basicProxyHandler(callback);
|
: basicProxyHandler(callback);
|
||||||
const proxy = new Proxy(target, handler);
|
const proxy = new Proxy(target, handler);
|
||||||
@@ -2621,7 +2622,7 @@ function wrapError(fn, hookName) {
|
|||||||
result.catch(() => { }),
|
result.catch(() => { }),
|
||||||
new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), 3000)),
|
new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), 3000)),
|
||||||
]).then((res) => {
|
]).then((res) => {
|
||||||
if (res === TIMEOUT && node.fiber === fiber) {
|
if (res === TIMEOUT && node.fiber === fiber && node.status <= 2) {
|
||||||
console.warn(timeoutError);
|
console.warn(timeoutError);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -3511,7 +3512,7 @@ function compileExprToArray(expr) {
|
|||||||
const localVars = new Set();
|
const localVars = new Set();
|
||||||
const tokens = tokenize(expr);
|
const tokens = tokenize(expr);
|
||||||
let i = 0;
|
let i = 0;
|
||||||
let stack = []; // to track last opening [ or {
|
let stack = []; // to track last opening (, [ or {
|
||||||
while (i < tokens.length) {
|
while (i < tokens.length) {
|
||||||
let token = tokens[i];
|
let token = tokens[i];
|
||||||
let prevToken = tokens[i - 1];
|
let prevToken = tokens[i - 1];
|
||||||
@@ -3520,10 +3521,12 @@ function compileExprToArray(expr) {
|
|||||||
switch (token.type) {
|
switch (token.type) {
|
||||||
case "LEFT_BRACE":
|
case "LEFT_BRACE":
|
||||||
case "LEFT_BRACKET":
|
case "LEFT_BRACKET":
|
||||||
|
case "LEFT_PAREN":
|
||||||
stack.push(token.type);
|
stack.push(token.type);
|
||||||
break;
|
break;
|
||||||
case "RIGHT_BRACE":
|
case "RIGHT_BRACE":
|
||||||
case "RIGHT_BRACKET":
|
case "RIGHT_BRACKET":
|
||||||
|
case "RIGHT_PAREN":
|
||||||
stack.pop();
|
stack.pop();
|
||||||
}
|
}
|
||||||
let isVar = token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value);
|
let isVar = token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value);
|
||||||
@@ -3635,6 +3638,13 @@ function isProp(tag, key) {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Returns a template literal that evaluates to str. You can add interpolation
|
||||||
|
* sigils into the string if required
|
||||||
|
*/
|
||||||
|
function toStringExpression(str) {
|
||||||
|
return `\`${str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/, "\\${")}\``;
|
||||||
|
}
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// BlockDescription
|
// BlockDescription
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -3815,15 +3825,14 @@ class CodeGenerator {
|
|||||||
mainCode.push(``);
|
mainCode.push(``);
|
||||||
for (let block of this.blocks) {
|
for (let block of this.blocks) {
|
||||||
if (block.dom) {
|
if (block.dom) {
|
||||||
let xmlString = block.asXmlString();
|
let xmlString = toStringExpression(block.asXmlString());
|
||||||
xmlString = xmlString.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
|
||||||
if (block.dynamicTagName) {
|
if (block.dynamicTagName) {
|
||||||
xmlString = xmlString.replace(/^<\w+/, `<\${tag || '${block.dom.nodeName}'}`);
|
xmlString = xmlString.replace(/^`<\w+/, `\`<\${tag || '${block.dom.nodeName}'}`);
|
||||||
xmlString = xmlString.replace(/\w+>$/, `\${tag || '${block.dom.nodeName}'}>`);
|
xmlString = xmlString.replace(/\w+>`$/, `\${tag || '${block.dom.nodeName}'}>\``);
|
||||||
mainCode.push(`let ${block.blockName} = tag => createBlock(\`${xmlString}\`);`);
|
mainCode.push(`let ${block.blockName} = tag => createBlock(${xmlString});`);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
mainCode.push(`let ${block.blockName} = createBlock(\`${xmlString}\`);`);
|
mainCode.push(`let ${block.blockName} = createBlock(${xmlString});`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4001,7 +4010,7 @@ class CodeGenerator {
|
|||||||
const isNewBlock = !block || forceNewBlock;
|
const isNewBlock = !block || forceNewBlock;
|
||||||
if (isNewBlock) {
|
if (isNewBlock) {
|
||||||
block = this.createBlock(block, "comment", ctx);
|
block = this.createBlock(block, "comment", ctx);
|
||||||
this.insertBlock(`comment(\`${ast.value}\`)`, block, {
|
this.insertBlock(`comment(${toStringExpression(ast.value)})`, block, {
|
||||||
...ctx,
|
...ctx,
|
||||||
forceNewBlock: forceNewBlock && !block,
|
forceNewBlock: forceNewBlock && !block,
|
||||||
});
|
});
|
||||||
@@ -4023,7 +4032,7 @@ class CodeGenerator {
|
|||||||
}
|
}
|
||||||
if (!block || forceNewBlock) {
|
if (!block || forceNewBlock) {
|
||||||
block = this.createBlock(block, "text", ctx);
|
block = this.createBlock(block, "text", ctx);
|
||||||
this.insertBlock(`text(\`${value}\`)`, block, {
|
this.insertBlock(`text(${toStringExpression(value)})`, block, {
|
||||||
...ctx,
|
...ctx,
|
||||||
forceNewBlock: forceNewBlock && !block,
|
forceNewBlock: forceNewBlock && !block,
|
||||||
});
|
});
|
||||||
@@ -4244,7 +4253,8 @@ class CodeGenerator {
|
|||||||
expr = compileExpr(ast.expr);
|
expr = compileExpr(ast.expr);
|
||||||
if (ast.defaultValue) {
|
if (ast.defaultValue) {
|
||||||
this.helpers.add("withDefault");
|
this.helpers.add("withDefault");
|
||||||
expr = `withDefault(${expr}, \`${ast.defaultValue}\`)`;
|
// FIXME: defaultValue is not translated
|
||||||
|
expr = `withDefault(${expr}, ${toStringExpression(ast.defaultValue)})`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!block || forceNewBlock) {
|
if (!block || forceNewBlock) {
|
||||||
@@ -4497,7 +4507,7 @@ class CodeGenerator {
|
|||||||
this.addLine(`${ctxVar}[zero] = ${bl};`);
|
this.addLine(`${ctxVar}[zero] = ${bl};`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const key = `key + \`${this.generateComponentKey()}\``;
|
const key = this.generateComponentKey();
|
||||||
if (isDynamic) {
|
if (isDynamic) {
|
||||||
const templateVar = generateId("template");
|
const templateVar = generateId("template");
|
||||||
if (!this.staticDefs.find((d) => d.id === "call")) {
|
if (!this.staticDefs.find((d) => d.id === "call")) {
|
||||||
@@ -4549,12 +4559,12 @@ class CodeGenerator {
|
|||||||
else {
|
else {
|
||||||
let value;
|
let value;
|
||||||
if (ast.defaultValue) {
|
if (ast.defaultValue) {
|
||||||
const defaultValue = ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue;
|
const defaultValue = toStringExpression(ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue);
|
||||||
if (ast.value) {
|
if (ast.value) {
|
||||||
value = `withDefault(${expr}, \`${defaultValue}\`)`;
|
value = `withDefault(${expr}, ${defaultValue})`;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
value = `\`${defaultValue}\``;
|
value = defaultValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -4565,12 +4575,12 @@ class CodeGenerator {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
generateComponentKey() {
|
generateComponentKey(currentKey = "key") {
|
||||||
const parts = [generateId("__")];
|
const parts = [generateId("__")];
|
||||||
for (let i = 0; i < this.target.loopLevel; i++) {
|
for (let i = 0; i < this.target.loopLevel; i++) {
|
||||||
parts.push(`\${key${i + 1}}`);
|
parts.push(`\${key${i + 1}}`);
|
||||||
}
|
}
|
||||||
return parts.join("__");
|
return `${currentKey} + \`${parts.join("__")}\``;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Formats a prop name and value into a string suitable to be inserted in the
|
* Formats a prop name and value into a string suitable to be inserted in the
|
||||||
@@ -4661,7 +4671,6 @@ class CodeGenerator {
|
|||||||
this.addLine(`${propVar}.slots = markRaw(Object.assign(${slotDef}, ${propVar}.slots))`);
|
this.addLine(`${propVar}.slots = markRaw(Object.assign(${slotDef}, ${propVar}.slots))`);
|
||||||
}
|
}
|
||||||
// cmap key
|
// cmap key
|
||||||
const key = this.generateComponentKey();
|
|
||||||
let expr;
|
let expr;
|
||||||
if (ast.isDynamic) {
|
if (ast.isDynamic) {
|
||||||
expr = generateId("Comp");
|
expr = generateId("Comp");
|
||||||
@@ -4677,7 +4686,7 @@ class CodeGenerator {
|
|||||||
// todo: check the forcenewblock condition
|
// todo: check the forcenewblock condition
|
||||||
this.insertAnchor(block);
|
this.insertAnchor(block);
|
||||||
}
|
}
|
||||||
let keyArg = `key + \`${key}\``;
|
let keyArg = this.generateComponentKey();
|
||||||
if (ctx.tKeyExpr) {
|
if (ctx.tKeyExpr) {
|
||||||
keyArg = `${ctx.tKeyExpr} + ${keyArg}`;
|
keyArg = `${ctx.tKeyExpr} + ${keyArg}`;
|
||||||
}
|
}
|
||||||
@@ -4750,7 +4759,7 @@ class CodeGenerator {
|
|||||||
}
|
}
|
||||||
let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
|
let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
|
||||||
if (isMultiple) {
|
if (isMultiple) {
|
||||||
key = `${key} + \`${this.generateComponentKey()}\``;
|
key = this.generateComponentKey(key);
|
||||||
}
|
}
|
||||||
const props = ast.attrs ? this.formatPropObject(ast.attrs) : [];
|
const props = ast.attrs ? this.formatPropObject(ast.attrs) : [];
|
||||||
const scope = this.getPropString(props, dynProps);
|
const scope = this.getPropString(props, dynProps);
|
||||||
@@ -4791,7 +4800,6 @@ class CodeGenerator {
|
|||||||
}
|
}
|
||||||
let { block } = ctx;
|
let { block } = ctx;
|
||||||
const name = this.compileInNewTarget("slot", ast.content, ctx);
|
const name = this.compileInNewTarget("slot", ast.content, ctx);
|
||||||
const key = this.generateComponentKey();
|
|
||||||
let ctxStr = "ctx";
|
let ctxStr = "ctx";
|
||||||
if (this.target.loopLevel || !this.hasSafeContext) {
|
if (this.target.loopLevel || !this.hasSafeContext) {
|
||||||
ctxStr = generateId("ctx");
|
ctxStr = generateId("ctx");
|
||||||
@@ -4804,7 +4812,8 @@ class CodeGenerator {
|
|||||||
expr: `app.createComponent(null, false, true, false, false)`,
|
expr: `app.createComponent(null, false, true, false, false)`,
|
||||||
});
|
});
|
||||||
const target = compileExpr(ast.target);
|
const target = compileExpr(ast.target);
|
||||||
const blockString = `${id}({target: ${target},slots: {'default': {__render: ${name}.bind(this), __ctx: ${ctxStr}}}}, key + \`${key}\`, node, ctx, Portal)`;
|
const key = this.generateComponentKey();
|
||||||
|
const blockString = `${id}({target: ${target},slots: {'default': {__render: ${name}.bind(this), __ctx: ${ctxStr}}}}, ${key}, node, ctx, Portal)`;
|
||||||
if (block) {
|
if (block) {
|
||||||
this.insertAnchor(block);
|
this.insertAnchor(block);
|
||||||
}
|
}
|
||||||
@@ -5537,7 +5546,7 @@ function compile(template, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// do not modify manually. This file is generated by the release script.
|
// do not modify manually. This file is generated by the release script.
|
||||||
const version = "2.2.8";
|
const version = "2.2.11";
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Scheduler
|
// Scheduler
|
||||||
@@ -5963,9 +5972,9 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate(name, templat
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export { App, Component, EventBus, OwlError, __info__, blockDom, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, validate, validateType, whenReady, xml };
|
export { App, Component, EventBus, OwlError, __info__, batched, blockDom, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, validate, validateType, whenReady, xml };
|
||||||
|
|
||||||
|
|
||||||
__info__.date = '2024-01-12T10:02:46.131Z';
|
__info__.date = '2024-06-17T13:31:12.099Z';
|
||||||
__info__.hash = '7b454da';
|
__info__.hash = 'e7f405c';
|
||||||
__info__.url = 'https://github.com/odoo/owl';
|
__info__.url = 'https://github.com/odoo/owl';
|
||||||
|
|||||||
@@ -41,9 +41,6 @@ const loadFile = (path) => {
|
|||||||
* Make an iframe, with all the js, css and xml properly injected.
|
* Make an iframe, with all the js, css and xml properly injected.
|
||||||
*/
|
*/
|
||||||
function makeCodeIframe(js, css, xml) {
|
function makeCodeIframe(js, css, xml) {
|
||||||
// escape backticks in the xml so they don't close the template string
|
|
||||||
const escapedXml = xml.replace(/`/g, '\\\`');
|
|
||||||
|
|
||||||
const iframe = document.createElement("iframe");
|
const iframe = document.createElement("iframe");
|
||||||
iframe.onload = () => {
|
iframe.onload = () => {
|
||||||
const doc = iframe.contentDocument;
|
const doc = iframe.contentDocument;
|
||||||
@@ -55,6 +52,8 @@ function makeCodeIframe(js, css, xml) {
|
|||||||
|
|
||||||
const script = doc.createElement("script");
|
const script = doc.createElement("script");
|
||||||
script.type = "module";
|
script.type = "module";
|
||||||
|
// escape characters with special meaning in template literals
|
||||||
|
const escapedXml = xml.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/, "\\${");
|
||||||
script.textContent = `const TEMPLATES = \`${escapedXml}\`\n${js}`;
|
script.textContent = `const TEMPLATES = \`${escapedXml}\`\n${js}`;
|
||||||
doc.body.appendChild(script);
|
doc.body.appendChild(script);
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@odoo/owl",
|
"name": "@odoo/owl",
|
||||||
"version": "2.2.8",
|
"version": "2.2.11",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@odoo/owl",
|
"name": "@odoo/owl",
|
||||||
"version": "2.2.8",
|
"version": "2.2.11",
|
||||||
"description": "Odoo Web Library (OWL)",
|
"description": "Odoo Web Library (OWL)",
|
||||||
"main": "dist/owl.cjs.js",
|
"main": "dist/owl.cjs.js",
|
||||||
"module": "dist/owl.es.js",
|
"module": "dist/owl.es.js",
|
||||||
|
|||||||
@@ -82,6 +82,14 @@ function isProp(tag: string, key: string): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a template literal that evaluates to str. You can add interpolation
|
||||||
|
* sigils into the string if required
|
||||||
|
*/
|
||||||
|
function toStringExpression(str: string) {
|
||||||
|
return `\`${str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/, "\\${")}\``;
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// BlockDescription
|
// BlockDescription
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -311,14 +319,13 @@ export class CodeGenerator {
|
|||||||
mainCode.push(``);
|
mainCode.push(``);
|
||||||
for (let block of this.blocks) {
|
for (let block of this.blocks) {
|
||||||
if (block.dom) {
|
if (block.dom) {
|
||||||
let xmlString = block.asXmlString();
|
let xmlString = toStringExpression(block.asXmlString());
|
||||||
xmlString = xmlString.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
|
||||||
if (block.dynamicTagName) {
|
if (block.dynamicTagName) {
|
||||||
xmlString = xmlString.replace(/^<\w+/, `<\${tag || '${block.dom.nodeName}'}`);
|
xmlString = xmlString.replace(/^`<\w+/, `\`<\${tag || '${block.dom.nodeName}'}`);
|
||||||
xmlString = xmlString.replace(/\w+>$/, `\${tag || '${block.dom.nodeName}'}>`);
|
xmlString = xmlString.replace(/\w+>`$/, `\${tag || '${block.dom.nodeName}'}>\``);
|
||||||
mainCode.push(`let ${block.blockName} = tag => createBlock(\`${xmlString}\`);`);
|
mainCode.push(`let ${block.blockName} = tag => createBlock(${xmlString});`);
|
||||||
} else {
|
} else {
|
||||||
mainCode.push(`let ${block.blockName} = createBlock(\`${xmlString}\`);`);
|
mainCode.push(`let ${block.blockName} = createBlock(${xmlString});`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -515,7 +522,7 @@ export class CodeGenerator {
|
|||||||
const isNewBlock = !block || forceNewBlock;
|
const isNewBlock = !block || forceNewBlock;
|
||||||
if (isNewBlock) {
|
if (isNewBlock) {
|
||||||
block = this.createBlock(block, "comment", ctx);
|
block = this.createBlock(block, "comment", ctx);
|
||||||
this.insertBlock(`comment(\`${ast.value}\`)`, block, {
|
this.insertBlock(`comment(${toStringExpression(ast.value)})`, block, {
|
||||||
...ctx,
|
...ctx,
|
||||||
forceNewBlock: forceNewBlock && !block,
|
forceNewBlock: forceNewBlock && !block,
|
||||||
});
|
});
|
||||||
@@ -539,7 +546,7 @@ export class CodeGenerator {
|
|||||||
|
|
||||||
if (!block || forceNewBlock) {
|
if (!block || forceNewBlock) {
|
||||||
block = this.createBlock(block, "text", ctx);
|
block = this.createBlock(block, "text", ctx);
|
||||||
this.insertBlock(`text(\`${value}\`)`, block, {
|
this.insertBlock(`text(${toStringExpression(value)})`, block, {
|
||||||
...ctx,
|
...ctx,
|
||||||
forceNewBlock: forceNewBlock && !block,
|
forceNewBlock: forceNewBlock && !block,
|
||||||
});
|
});
|
||||||
@@ -774,7 +781,8 @@ export class CodeGenerator {
|
|||||||
expr = compileExpr(ast.expr);
|
expr = compileExpr(ast.expr);
|
||||||
if (ast.defaultValue) {
|
if (ast.defaultValue) {
|
||||||
this.helpers.add("withDefault");
|
this.helpers.add("withDefault");
|
||||||
expr = `withDefault(${expr}, \`${ast.defaultValue}\`)`;
|
// FIXME: defaultValue is not translated
|
||||||
|
expr = `withDefault(${expr}, ${toStringExpression(ast.defaultValue)})`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!block || forceNewBlock) {
|
if (!block || forceNewBlock) {
|
||||||
@@ -1039,7 +1047,7 @@ export class CodeGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = `key + \`${this.generateComponentKey()}\``;
|
const key = this.generateComponentKey();
|
||||||
if (isDynamic) {
|
if (isDynamic) {
|
||||||
const templateVar = generateId("template");
|
const templateVar = generateId("template");
|
||||||
if (!this.staticDefs.find((d) => d.id === "call")) {
|
if (!this.staticDefs.find((d) => d.id === "call")) {
|
||||||
@@ -1091,11 +1099,13 @@ export class CodeGenerator {
|
|||||||
} else {
|
} else {
|
||||||
let value: string;
|
let value: string;
|
||||||
if (ast.defaultValue) {
|
if (ast.defaultValue) {
|
||||||
const defaultValue = ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue;
|
const defaultValue = toStringExpression(
|
||||||
|
ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue
|
||||||
|
);
|
||||||
if (ast.value) {
|
if (ast.value) {
|
||||||
value = `withDefault(${expr}, \`${defaultValue}\`)`;
|
value = `withDefault(${expr}, ${defaultValue})`;
|
||||||
} else {
|
} else {
|
||||||
value = `\`${defaultValue}\``;
|
value = defaultValue;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
value = expr;
|
value = expr;
|
||||||
@@ -1106,12 +1116,12 @@ export class CodeGenerator {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
generateComponentKey() {
|
generateComponentKey(currentKey: string = "key") {
|
||||||
const parts = [generateId("__")];
|
const parts = [generateId("__")];
|
||||||
for (let i = 0; i < this.target.loopLevel; i++) {
|
for (let i = 0; i < this.target.loopLevel; i++) {
|
||||||
parts.push(`\${key${i + 1}}`);
|
parts.push(`\${key${i + 1}}`);
|
||||||
}
|
}
|
||||||
return parts.join("__");
|
return `${currentKey} + \`${parts.join("__")}\``;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1214,7 +1224,6 @@ export class CodeGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// cmap key
|
// cmap key
|
||||||
const key = this.generateComponentKey();
|
|
||||||
let expr: string;
|
let expr: string;
|
||||||
if (ast.isDynamic) {
|
if (ast.isDynamic) {
|
||||||
expr = generateId("Comp");
|
expr = generateId("Comp");
|
||||||
@@ -1232,7 +1241,7 @@ export class CodeGenerator {
|
|||||||
this.insertAnchor(block);
|
this.insertAnchor(block);
|
||||||
}
|
}
|
||||||
|
|
||||||
let keyArg = `key + \`${key}\``;
|
let keyArg = this.generateComponentKey();
|
||||||
if (ctx.tKeyExpr) {
|
if (ctx.tKeyExpr) {
|
||||||
keyArg = `${ctx.tKeyExpr} + ${keyArg}`;
|
keyArg = `${ctx.tKeyExpr} + ${keyArg}`;
|
||||||
}
|
}
|
||||||
@@ -1311,7 +1320,7 @@ export class CodeGenerator {
|
|||||||
}
|
}
|
||||||
let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
|
let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
|
||||||
if (isMultiple) {
|
if (isMultiple) {
|
||||||
key = `${key} + \`${this.generateComponentKey()}\``;
|
key = this.generateComponentKey(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = ast.attrs ? this.formatPropObject(ast.attrs) : [];
|
const props = ast.attrs ? this.formatPropObject(ast.attrs) : [];
|
||||||
@@ -1354,7 +1363,6 @@ export class CodeGenerator {
|
|||||||
|
|
||||||
let { block } = ctx;
|
let { block } = ctx;
|
||||||
const name = this.compileInNewTarget("slot", ast.content, ctx);
|
const name = this.compileInNewTarget("slot", ast.content, ctx);
|
||||||
const key = this.generateComponentKey();
|
|
||||||
let ctxStr = "ctx";
|
let ctxStr = "ctx";
|
||||||
if (this.target.loopLevel || !this.hasSafeContext) {
|
if (this.target.loopLevel || !this.hasSafeContext) {
|
||||||
ctxStr = generateId("ctx");
|
ctxStr = generateId("ctx");
|
||||||
@@ -1368,7 +1376,8 @@ export class CodeGenerator {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const target = compileExpr(ast.target);
|
const target = compileExpr(ast.target);
|
||||||
const blockString = `${id}({target: ${target},slots: {'default': {__render: ${name}.bind(this), __ctx: ${ctxStr}}}}, key + \`${key}\`, node, ctx, Portal)`;
|
const key = this.generateComponentKey();
|
||||||
|
const blockString = `${id}({target: ${target},slots: {'default': {__render: ${name}.bind(this), __ctx: ${ctxStr}}}}, ${key}, node, ctx, Portal)`;
|
||||||
if (block) {
|
if (block) {
|
||||||
this.insertAnchor(block);
|
this.insertAnchor(block);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ export function compileExprToArray(expr: string): Token[] {
|
|||||||
const localVars = new Set<string>();
|
const localVars = new Set<string>();
|
||||||
const tokens = tokenize(expr);
|
const tokens = tokenize(expr);
|
||||||
let i = 0;
|
let i = 0;
|
||||||
let stack = []; // to track last opening [ or {
|
let stack = []; // to track last opening (, [ or {
|
||||||
|
|
||||||
while (i < tokens.length) {
|
while (i < tokens.length) {
|
||||||
let token = tokens[i];
|
let token = tokens[i];
|
||||||
@@ -279,10 +279,12 @@ export function compileExprToArray(expr: string): Token[] {
|
|||||||
switch (token.type) {
|
switch (token.type) {
|
||||||
case "LEFT_BRACE":
|
case "LEFT_BRACE":
|
||||||
case "LEFT_BRACKET":
|
case "LEFT_BRACKET":
|
||||||
|
case "LEFT_PAREN":
|
||||||
stack.push(token.type);
|
stack.push(token.type);
|
||||||
break;
|
break;
|
||||||
case "RIGHT_BRACE":
|
case "RIGHT_BRACE":
|
||||||
case "RIGHT_BRACKET":
|
case "RIGHT_BRACKET":
|
||||||
|
case "RIGHT_PAREN":
|
||||||
stack.pop();
|
stack.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export { useComponent, useState } from "./component_node";
|
|||||||
export { status } from "./status";
|
export { status } from "./status";
|
||||||
export { reactive, markRaw, toRaw } from "./reactivity";
|
export { reactive, markRaw, toRaw } from "./reactivity";
|
||||||
export { useEffect, useEnv, useExternalListener, useRef, useChildSubEnv, useSubEnv } from "./hooks";
|
export { useEffect, useEnv, useExternalListener, useRef, useChildSubEnv, useSubEnv } from "./hooks";
|
||||||
export { EventBus, whenReady, loadFile, markup } from "./utils";
|
export { batched, EventBus, whenReady, loadFile, markup } from "./utils";
|
||||||
export {
|
export {
|
||||||
onWillStart,
|
onWillStart,
|
||||||
onMounted,
|
onMounted,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function wrapError(fn: (...args: any[]) => any, hookName: string) {
|
|||||||
result.catch(() => {}),
|
result.catch(() => {}),
|
||||||
new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), 3000)),
|
new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), 3000)),
|
||||||
]).then((res) => {
|
]).then((res) => {
|
||||||
if (res === TIMEOUT && node.fiber === fiber) {
|
if (res === TIMEOUT && node.fiber === fiber && node.status <= 2) {
|
||||||
console.warn(timeoutError);
|
console.warn(timeoutError);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ type CollectionRawType = "Set" | "Map" | "WeakMap";
|
|||||||
const objectToString = Object.prototype.toString;
|
const objectToString = Object.prototype.toString;
|
||||||
const objectHasOwnProperty = Object.prototype.hasOwnProperty;
|
const objectHasOwnProperty = Object.prototype.hasOwnProperty;
|
||||||
|
|
||||||
const SUPPORTED_RAW_TYPES = new Set(["Object", "Array", "Set", "Map", "WeakMap"]);
|
// Use arrays because Array.includes is faster than Set.has for small arrays
|
||||||
const COLLECTION_RAWTYPES = new Set(["Set", "Map", "WeakMap"]);
|
const SUPPORTED_RAW_TYPES = ["Object", "Array", "Set", "Map", "WeakMap"];
|
||||||
|
const COLLECTION_RAW_TYPES = ["Set", "Map", "WeakMap"];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* extract "RawType" from strings like "[object RawType]" => this lets us ignore
|
* extract "RawType" from strings like "[object RawType]" => this lets us ignore
|
||||||
@@ -45,7 +46,7 @@ function canBeMadeReactive(value: any): boolean {
|
|||||||
if (typeof value !== "object") {
|
if (typeof value !== "object") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return SUPPORTED_RAW_TYPES.has(rawType(value));
|
return SUPPORTED_RAW_TYPES.includes(rawType(value));
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Creates a reactive from the given object/callback if possible and returns it,
|
* Creates a reactive from the given object/callback if possible and returns it,
|
||||||
@@ -220,7 +221,7 @@ export function reactive<T extends Target>(target: T, callback: Callback = NO_CA
|
|||||||
const reactivesForTarget = reactiveCache.get(target)!;
|
const reactivesForTarget = reactiveCache.get(target)!;
|
||||||
if (!reactivesForTarget.has(callback)) {
|
if (!reactivesForTarget.has(callback)) {
|
||||||
const targetRawType = rawType(target);
|
const targetRawType = rawType(target);
|
||||||
const handler = COLLECTION_RAWTYPES.has(targetRawType)
|
const handler = COLLECTION_RAW_TYPES.includes(targetRawType)
|
||||||
? collectionsProxyHandler(target as Collection, callback, targetRawType as CollectionRawType)
|
? collectionsProxyHandler(target as Collection, callback, targetRawType as CollectionRawType)
|
||||||
: basicProxyHandler<T>(callback);
|
: basicProxyHandler<T>(callback);
|
||||||
const proxy = new Proxy(target, handler as ProxyHandler<T>) as Reactive<T>;
|
const proxy = new Proxy(target, handler as ProxyHandler<T>) as Reactive<T>;
|
||||||
|
|||||||
@@ -1,16 +1,7 @@
|
|||||||
import { OwlError } from "../common/owl_error";
|
import { OwlError } from "../common/owl_error";
|
||||||
import { toRaw } from "./reactivity";
|
import { toRaw } from "./reactivity";
|
||||||
|
|
||||||
type BaseType =
|
type BaseType = { new (...args: any[]): any } | true | "*";
|
||||||
| typeof String
|
|
||||||
| typeof Boolean
|
|
||||||
| typeof Number
|
|
||||||
| typeof Date
|
|
||||||
| typeof Object
|
|
||||||
| typeof Array
|
|
||||||
| typeof Function
|
|
||||||
| true
|
|
||||||
| "*";
|
|
||||||
|
|
||||||
interface TypeInfo {
|
interface TypeInfo {
|
||||||
type?: TypeDescription;
|
type?: TypeDescription;
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
// do not modify manually. This file is generated by the release script.
|
// do not modify manually. This file is generated by the release script.
|
||||||
export const version = "2.2.8";
|
export const version = "2.2.11";
|
||||||
|
|||||||
@@ -1,5 +1,38 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`comments comment node with backslash at top level 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return comment(\` \\\\\\\\ \`);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`comments comment node with backtick at top-level 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return comment(\` \\\\\` \`);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`comments comment node with interpolation sigil at top level 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return comment(\` \\\\\${very cool} \`);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`comments only a comment 1`] = `
|
exports[`comments only a comment 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -341,6 +341,39 @@ exports[`simple templates, mostly static template with t tag with multiple conte
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`simple templates, mostly static text node with backslash at top level 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(\`\\\\\\\\\`);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`simple templates, mostly static text node with backtick at top-level 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(\`\\\\\`\`);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`simple templates, mostly static text node with interpolation sigil at top level 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(\`\\\\\${very cool}\`);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`simple templates, mostly static two t-escs next to each other 1`] = `
|
exports[`simple templates, mostly static two t-escs next to each other 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,5 +1,41 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`t-esc default with backslash at top level 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { withDefault } = helpers;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(withDefault(undefined, \`\\\\\\\\\`));
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`t-esc default with backtick at top-level 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { withDefault } = helpers;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(withDefault(undefined, \`\\\\\`\`));
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`t-esc default with interpolation sigil at top level 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { withDefault } = helpers;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(withDefault(undefined, \`\\\\\${very cool}\`));
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`t-esc div with falsy values 1`] = `
|
exports[`t-esc div with falsy values 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,5 +1,50 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`t-set body with backslash at top level 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { isBoundary, withDefault, setContextValue } = helpers;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
ctx = Object.create(ctx);
|
||||||
|
ctx[isBoundary] = 1
|
||||||
|
setContextValue(ctx, \\"value\\", \`\\\\\\\\\`);
|
||||||
|
return text(ctx['value']);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`t-set body with backtick at top-level 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { isBoundary, withDefault, setContextValue } = helpers;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
ctx = Object.create(ctx);
|
||||||
|
ctx[isBoundary] = 1
|
||||||
|
setContextValue(ctx, \\"value\\", \`\\\\\`\`);
|
||||||
|
return text(ctx['value']);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`t-set body with interpolation sigil at top level 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { isBoundary, withDefault, setContextValue } = helpers;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
ctx = Object.create(ctx);
|
||||||
|
ctx[isBoundary] = 1
|
||||||
|
setContextValue(ctx, \\"value\\", \`\\\\\${very cool}\`);
|
||||||
|
return text(ctx['value']);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`t-set evaluate value expression 1`] = `
|
exports[`t-set evaluate value expression 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -26,4 +26,19 @@ describe("comments", () => {
|
|||||||
</div>`;
|
</div>`;
|
||||||
expect(renderToString(template)).toBe("<div><span>true</span></div>");
|
expect(renderToString(template)).toBe("<div><span>true</span></div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("comment node with backslash at top level", () => {
|
||||||
|
const template = "<!-- \\ -->";
|
||||||
|
expect(renderToString(template)).toBe("<!-- \\ -->");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("comment node with backtick at top-level", () => {
|
||||||
|
const template = "<!-- ` -->";
|
||||||
|
expect(renderToString(template)).toBe("<!-- ` -->");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("comment node with interpolation sigil at top level", () => {
|
||||||
|
const template = "<!-- ${very cool} -->";
|
||||||
|
expect(renderToString(template)).toBe("<!-- ${very cool} -->");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -174,6 +174,9 @@ describe("expression evaluation", () => {
|
|||||||
expect(compileExpr("list.data.map((data) => data)")).toBe(
|
expect(compileExpr("list.data.map((data) => data)")).toBe(
|
||||||
"ctx['list'].data.map((_data)=>_data)"
|
"ctx['list'].data.map((_data)=>_data)"
|
||||||
);
|
);
|
||||||
|
expect(compileExpr("(ev) => { myFunc(v1, v2, ev.target.value); }")).toBe(
|
||||||
|
"(_ev)=>{ctx['myFunc'](ctx['v1'],ctx['v2'],_ev.target.value);}"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
test.skip("arrow functions: not yet supported", () => {
|
test.skip("arrow functions: not yet supported", () => {
|
||||||
// e is added to localvars in inline_expression but not removed after the arrow func body
|
// e is added to localvars in inline_expression but not removed after the arrow func body
|
||||||
|
|||||||
@@ -154,4 +154,19 @@ describe("simple templates, mostly static", () => {
|
|||||||
</div>`;
|
</div>`;
|
||||||
expect(renderToString(template, { a: "a", b: "b", c: "c" })).toBe("<div>abLoadingc</div>");
|
expect(renderToString(template, { a: "a", b: "b", c: "c" })).toBe("<div>abLoadingc</div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("text node with backslash at top level", () => {
|
||||||
|
const template = "\\";
|
||||||
|
expect(renderToString(template)).toBe("\\");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("text node with backtick at top-level", () => {
|
||||||
|
const template = "`";
|
||||||
|
expect(renderToString(template)).toBe("`");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("text node with interpolation sigil at top level", () => {
|
||||||
|
const template = "${very cool}";
|
||||||
|
expect(renderToString(template)).toBe("${very cool}");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -121,4 +121,19 @@ describe("t-esc", () => {
|
|||||||
mount(bdom, fixture);
|
mount(bdom, fixture);
|
||||||
expect(fixture.querySelector("span")!.textContent).toBe("<p>escaped</p>");
|
expect(fixture.querySelector("span")!.textContent).toBe("<p>escaped</p>");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("default with backslash at top level", () => {
|
||||||
|
const template = '<t t-esc="undefined">\\</t>';
|
||||||
|
expect(renderToString(template)).toBe("\\");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("default with backtick at top-level", () => {
|
||||||
|
const template = '<t t-esc="undefined">`</t>';
|
||||||
|
expect(renderToString(template)).toBe("`");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("default with interpolation sigil at top level", () => {
|
||||||
|
const template = '<t t-esc="undefined">${very cool}</t>';
|
||||||
|
expect(renderToString(template)).toBe("${very cool}");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -54,6 +54,21 @@ describe("t-set", () => {
|
|||||||
expect(renderToString(template)).toBe("ok");
|
expect(renderToString(template)).toBe("ok");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("body with backslash at top level", () => {
|
||||||
|
const template = '<t t-set="value">\\</t><t t-esc="value"/>';
|
||||||
|
expect(renderToString(template)).toBe("\\");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("body with backtick at top-level", () => {
|
||||||
|
const template = '<t t-set="value">`</t><t t-esc="value"/>';
|
||||||
|
expect(renderToString(template)).toBe("`");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("body with interpolation sigil at top level", () => {
|
||||||
|
const template = '<t t-set="value">${very cool}</t><t t-esc="value"/>';
|
||||||
|
expect(renderToString(template)).toBe("${very cool}");
|
||||||
|
});
|
||||||
|
|
||||||
test("set from body literal (with t-if/t-else", () => {
|
test("set from body literal (with t-if/t-else", () => {
|
||||||
const template = `
|
const template = `
|
||||||
<t>
|
<t>
|
||||||
|
|||||||
@@ -683,6 +683,19 @@ exports[`lifecycle hooks sub widget (inside sub node): hooks are correctly calle
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`lifecycle hooks timeout in onWillStart doesn't emit a warning if app is destroyed 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<span/>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`lifecycle hooks timeout in onWillStart emits a warning 1`] = `
|
exports[`lifecycle hooks timeout in onWillStart emits a warning 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -167,6 +167,45 @@ exports[`props validation can specify that additional props are allowed (object)
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`props validation can use custom class as type 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
const comp1 = app.createComponent(\`Child\`, true, false, false, [\\"customObj\\"]);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
const props1 = {customObj: ctx['customObj']};
|
||||||
|
helpers.validateProps(\`Child\`, props1, this);
|
||||||
|
return comp1(props1, key + \`__1\`, node, this, null);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`props validation can use custom class as type 2`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(ctx['props'].customObj.val);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`props validation can use custom class as type: validation failure 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
const comp1 = app.createComponent(\`Child\`, true, false, false, [\\"customObj\\"]);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
const props1 = {customObj: ctx['customObj']};
|
||||||
|
helpers.validateProps(\`Child\`, props1, this);
|
||||||
|
return comp1(props1, key + \`__1\`, node, this, null);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`props validation can validate a prop with multiple types 1`] = `
|
exports[`props validation can validate a prop with multiple types 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { App, Component, mount, onMounted, onWillStart, useState, xml } from "../../src";
|
|
||||||
import {
|
import {
|
||||||
|
App,
|
||||||
|
Component,
|
||||||
|
mount,
|
||||||
|
useState,
|
||||||
|
xml,
|
||||||
onWillPatch,
|
onWillPatch,
|
||||||
onWillUnmount,
|
onWillUnmount,
|
||||||
onPatched,
|
onPatched,
|
||||||
@@ -7,7 +11,9 @@ import {
|
|||||||
onWillRender,
|
onWillRender,
|
||||||
onWillDestroy,
|
onWillDestroy,
|
||||||
onRendered,
|
onRendered,
|
||||||
} from "../../src/runtime/lifecycle_hooks";
|
onMounted,
|
||||||
|
onWillStart,
|
||||||
|
} from "../../src";
|
||||||
import { status } from "../../src/runtime/status";
|
import { status } from "../../src/runtime/status";
|
||||||
import {
|
import {
|
||||||
elem,
|
elem,
|
||||||
@@ -117,24 +123,60 @@ describe("lifecycle hooks", () => {
|
|||||||
timeoutCbs[++timeoutId] = cb;
|
timeoutCbs[++timeoutId] = cb;
|
||||||
return timeoutId;
|
return timeoutId;
|
||||||
}) as any;
|
}) as any;
|
||||||
class Test extends Component {
|
try {
|
||||||
static template = xml`<span/>`;
|
class Test extends Component {
|
||||||
setup() {
|
static template = xml`<span/>`;
|
||||||
onWillStart(() => new Promise(() => {}));
|
setup() {
|
||||||
|
onWillStart(() => new Promise(() => {}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
mount(Test, fixture, { test: true });
|
||||||
|
nextTick();
|
||||||
|
for (const id in timeoutCbs) {
|
||||||
|
timeoutCbs[id]();
|
||||||
|
delete timeoutCbs[id];
|
||||||
|
}
|
||||||
|
await nextMicroTick();
|
||||||
|
await nextMicroTick();
|
||||||
|
expect(console.warn).toHaveBeenCalledTimes(1);
|
||||||
|
expect(warnArgs![0]!.message).toBe("onWillStart's promise hasn't resolved after 3 seconds");
|
||||||
|
} finally {
|
||||||
|
console.warn = warn;
|
||||||
|
window.setTimeout = setTimeout;
|
||||||
}
|
}
|
||||||
mount(Test, fixture, { test: true });
|
});
|
||||||
nextTick();
|
|
||||||
for (const id in timeoutCbs) {
|
test("timeout in onWillStart doesn't emit a warning if app is destroyed", async () => {
|
||||||
timeoutCbs[id]();
|
const { warn } = console;
|
||||||
delete timeoutCbs[id];
|
console.warn = jest.fn();
|
||||||
|
const { setTimeout } = window;
|
||||||
|
let timeoutCbs: any = {};
|
||||||
|
let timeoutId = 0;
|
||||||
|
window.setTimeout = ((cb: any) => {
|
||||||
|
timeoutCbs[++timeoutId] = cb;
|
||||||
|
return timeoutId;
|
||||||
|
}) as any;
|
||||||
|
try {
|
||||||
|
class Test extends Component {
|
||||||
|
static template = xml`<span/>`;
|
||||||
|
setup() {
|
||||||
|
onWillStart(() => new Promise(() => {}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const app = new App(Test, { test: true });
|
||||||
|
app.mount(fixture);
|
||||||
|
app.destroy();
|
||||||
|
for (const id in timeoutCbs) {
|
||||||
|
timeoutCbs[id]();
|
||||||
|
delete timeoutCbs[id];
|
||||||
|
}
|
||||||
|
await nextMicroTick();
|
||||||
|
await nextMicroTick();
|
||||||
|
expect(console.warn).toHaveBeenCalledTimes(0);
|
||||||
|
} finally {
|
||||||
|
console.warn = warn;
|
||||||
|
window.setTimeout = setTimeout;
|
||||||
}
|
}
|
||||||
await nextMicroTick();
|
|
||||||
await nextMicroTick();
|
|
||||||
expect(console.warn).toHaveBeenCalledTimes(1);
|
|
||||||
expect(warnArgs![0]!.message).toBe("onWillStart's promise hasn't resolved after 3 seconds");
|
|
||||||
console.warn = warn;
|
|
||||||
window.setTimeout = setTimeout;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("timeout in onWillUpdateProps emits a warning", async () => {
|
test("timeout in onWillUpdateProps emits a warning", async () => {
|
||||||
@@ -162,25 +204,28 @@ describe("lifecycle hooks", () => {
|
|||||||
return timeoutId;
|
return timeoutId;
|
||||||
}) as any;
|
}) as any;
|
||||||
|
|
||||||
parent.state.prop = 2;
|
try {
|
||||||
let tick = nextTick();
|
parent.state.prop = 2;
|
||||||
for (const id in timeoutCbs) {
|
let tick = nextTick();
|
||||||
timeoutCbs[id]();
|
for (const id in timeoutCbs) {
|
||||||
delete timeoutCbs[id];
|
timeoutCbs[id]();
|
||||||
|
delete timeoutCbs[id];
|
||||||
|
}
|
||||||
|
await tick;
|
||||||
|
tick = nextTick();
|
||||||
|
for (const id in timeoutCbs) {
|
||||||
|
timeoutCbs[id]();
|
||||||
|
delete timeoutCbs[id];
|
||||||
|
}
|
||||||
|
await tick;
|
||||||
|
expect(console.warn).toHaveBeenCalledTimes(1);
|
||||||
|
expect(warnArgs![0]!.message).toBe(
|
||||||
|
"onWillUpdateProps's promise hasn't resolved after 3 seconds"
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
console.warn = warn;
|
||||||
|
window.setTimeout = setTimeout;
|
||||||
}
|
}
|
||||||
await tick;
|
|
||||||
tick = nextTick();
|
|
||||||
for (const id in timeoutCbs) {
|
|
||||||
timeoutCbs[id]();
|
|
||||||
delete timeoutCbs[id];
|
|
||||||
}
|
|
||||||
await tick;
|
|
||||||
expect(console.warn).toHaveBeenCalledTimes(1);
|
|
||||||
expect(warnArgs![0]!.message).toBe(
|
|
||||||
"onWillUpdateProps's promise hasn't resolved after 3 seconds"
|
|
||||||
);
|
|
||||||
console.warn = warn;
|
|
||||||
window.setTimeout = setTimeout;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("mounted hook is called if mounted in DOM", async () => {
|
test("mounted hook is called if mounted in DOM", async () => {
|
||||||
|
|||||||
@@ -829,6 +829,52 @@ describe("props validation", () => {
|
|||||||
expect(error!).toBeDefined();
|
expect(error!).toBeDefined();
|
||||||
expect(error!.message).toBe("Invalid props for component 'Child': 'message' is missing");
|
expect(error!.message).toBe("Invalid props for component 'Child': 'message' is missing");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("can use custom class as type", async () => {
|
||||||
|
class CustomClass {
|
||||||
|
val = "hey";
|
||||||
|
}
|
||||||
|
class Child extends Component {
|
||||||
|
static props = { customObj: CustomClass };
|
||||||
|
static template = xml`<t t-esc="props.customObj.val"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component {
|
||||||
|
static components = { Child };
|
||||||
|
static template = xml`<Child customObj="customObj" />`;
|
||||||
|
customObj = new CustomClass();
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new App(Parent, { test: true });
|
||||||
|
await app.mount(fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("hey");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can use custom class as type: validation failure", async () => {
|
||||||
|
class CustomClass {}
|
||||||
|
class Child extends Component {
|
||||||
|
static props = { customObj: CustomClass };
|
||||||
|
static template = xml`<div>hey</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component {
|
||||||
|
static components = { Child };
|
||||||
|
static template = xml`<Child customObj="customObj" />`;
|
||||||
|
customObj = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new App(Parent, { test: true });
|
||||||
|
let error: OwlError | undefined;
|
||||||
|
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
|
||||||
|
await expect(nextAppError(app)).resolves.toThrow(
|
||||||
|
"Invalid props for component 'Child': 'customObj' is not a customclass"
|
||||||
|
);
|
||||||
|
await mountProm;
|
||||||
|
expect(error!).toBeDefined();
|
||||||
|
expect(error!.message).toBe(
|
||||||
|
"Invalid props for component 'Child': 'customObj' is not a customclass"
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -4,6 +4,28 @@ All notable changes to the "owl-vision" extension will be documented in this fil
|
|||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
|
||||||
|
## [0.1.0] - 2024-11-06
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Basic autocomplete in xml files. This includes autocompletion for elements, components,
|
||||||
|
props, attributes, and javascript expressions.
|
||||||
|
|
||||||
|
The current implementation, while relatively simple, has a couple of drawbacks:
|
||||||
|
- Javascript imports are not resolved by the xml autocomplete, this means that it does not
|
||||||
|
understand the types of imported functions or objects. That said, I've added custom support
|
||||||
|
for frequently used Owl imports, namely `useState` and `useRef`. You can add more in
|
||||||
|
the settings if needed.
|
||||||
|
- The autocomplete is limited to templates directly linked to components, sub-templates
|
||||||
|
used via t-call will not get autocompletion as no component/context can be bound to them.
|
||||||
|
|
||||||
|
- "Go To Definition" support for props and javascript expressions in xml
|
||||||
|
- Support for the following directives: t-att, t-model, t-tag, t-debug, t-log
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Changed t-else syntax highlight from dynamic to static attribute
|
||||||
|
|
||||||
## [0.0.2] - 2023-2-11
|
## [0.0.2] - 2023-2-11
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
Generated
+5557
-5527
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
|||||||
"description": "Owl framework extension that highlights templates and ease navigation between components and templates.",
|
"description": "Owl framework extension that highlights templates and ease navigation between components and templates.",
|
||||||
"publisher": "Odoo",
|
"publisher": "Odoo",
|
||||||
"license": "LGPL-3.0-only",
|
"license": "LGPL-3.0-only",
|
||||||
"version": "0.0.2",
|
"version": "0.1.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/odoo/owl/tree/master/tools/owl-vision"
|
"url": "https://github.com/odoo/owl/tree/master/tools/owl-vision"
|
||||||
@@ -63,6 +63,13 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "**/node_modules/**,**/lib/**,**/tests/**",
|
"default": "**/node_modules/**,**/lib/**,**/tests/**",
|
||||||
"description": "Files to exclude in search"
|
"description": "Files to exclude in search"
|
||||||
|
},
|
||||||
|
"owl-vision.autocomplete-mocks": {
|
||||||
|
"order": 2,
|
||||||
|
"type": "string",
|
||||||
|
"editPresentation": "multilineText",
|
||||||
|
"default": "/**\n* @template T\n* @param {T} obj\n* @returns {T}\n*/\nfunction useState(obj) {}\n\n/**\n* @typedef {Object} Ref\n* @property {HTMLElement} el\n*/\n/**\n* @returns {Ref}\n*/\nfunction useRef(name) {}",
|
||||||
|
"description": "Mocks for functions or object that are imported but not resolved by the autcomplete. Add docstring comments for them to work properly."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -128,6 +135,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "20.2.5",
|
"@types/node": "20.2.5",
|
||||||
"@types/vscode": "^1.73.0",
|
"@types/vscode": "^1.73.0",
|
||||||
|
"@types/xmldoc": "^1.1.9",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.59.8",
|
"@typescript-eslint/eslint-plugin": "^5.59.8",
|
||||||
"@typescript-eslint/parser": "^5.59.8",
|
"@typescript-eslint/parser": "^5.59.8",
|
||||||
"@vscode/test-electron": "^2.3.2",
|
"@vscode/test-electron": "^2.3.2",
|
||||||
@@ -135,5 +143,8 @@
|
|||||||
"esbuild": "^0.19.5",
|
"esbuild": "^0.19.5",
|
||||||
"eslint": "^8.41.0",
|
"eslint": "^8.41.0",
|
||||||
"typescript": "^5.1.3"
|
"typescript": "^5.1.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"xmldoc": "^1.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,7 +62,6 @@ export const propsAttributes = createAttributePatterns("props-attributes", {
|
|||||||
export const owlAttributesDynamic = createAttributePatterns("owl-attributes-dynamic", {
|
export const owlAttributesDynamic = createAttributePatterns("owl-attributes-dynamic", {
|
||||||
match: [
|
match: [
|
||||||
"t-if",
|
"t-if",
|
||||||
"t-else",
|
|
||||||
"t-elif",
|
"t-elif",
|
||||||
"t-foreach",
|
"t-foreach",
|
||||||
"t-as",
|
"t-as",
|
||||||
@@ -75,6 +74,10 @@ export const owlAttributesDynamic = createAttributePatterns("owl-attributes-dyna
|
|||||||
"t-value",
|
"t-value",
|
||||||
"t-portal",
|
"t-portal",
|
||||||
"t-slot-scope",
|
"t-slot-scope",
|
||||||
|
"t-att",
|
||||||
|
"t-tag",
|
||||||
|
"t-log",
|
||||||
|
"t-model",
|
||||||
"t-att-[a-z_:.-]+",
|
"t-att-[a-z_:.-]+",
|
||||||
"t-on-[a-z_:.-]+"
|
"t-on-[a-z_:.-]+"
|
||||||
].join("|"),
|
].join("|"),
|
||||||
@@ -86,12 +89,13 @@ export const owlAttributesDynamic = createAttributePatterns("owl-attributes-dyna
|
|||||||
export const owlAttributesStatic = createAttributePatterns("owl-attributes-static", {
|
export const owlAttributesStatic = createAttributePatterns("owl-attributes-static", {
|
||||||
match: [
|
match: [
|
||||||
"t-name",
|
"t-name",
|
||||||
|
"t-else",
|
||||||
"t-ref",
|
"t-ref",
|
||||||
"t-set-slot",
|
"t-set-slot",
|
||||||
"t-model",
|
|
||||||
"t-inherit",
|
"t-inherit",
|
||||||
"t-inherit-mode",
|
"t-inherit-mode",
|
||||||
"t-translation"
|
"t-translation",
|
||||||
|
"t-debug",
|
||||||
].join("|"),
|
].join("|"),
|
||||||
attributeName: "owl.attribute owl.attribute.static",
|
attributeName: "owl.attribute owl.attribute.static",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,18 +1,43 @@
|
|||||||
{
|
{
|
||||||
"Basic owl component": {
|
"Basic OWL Component": {
|
||||||
"prefix": "owlcomponent",
|
"prefix": "owlcomponent",
|
||||||
|
"scope": "javascript,typescript",
|
||||||
"body": [
|
"body": [
|
||||||
"export class ${1:component-name} extends Component {",
|
"import { Component } from \"@odoo/owl\";",
|
||||||
" static template = \"${2:template-name}\";",
|
"",
|
||||||
|
"class ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/g}} extends ${2:Component} {",
|
||||||
|
"",
|
||||||
|
" static template = \"${3:${RELATIVE_FILEPATH/(.*[\\|\\/])??([a-zA-Z_]+)([\\|\\/]static[\\|\\/].*)/${2}/g}}.${4:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/g}}\";",
|
||||||
" static components = {};",
|
" static components = {};",
|
||||||
" static props = {};",
|
" static props = {};",
|
||||||
"",
|
"",
|
||||||
" setup() {",
|
" setup() {",
|
||||||
"",
|
" ${5:super.setup();}",
|
||||||
" }",
|
" }",
|
||||||
|
"",
|
||||||
|
" ${6:// Do Something}",
|
||||||
"}",
|
"}",
|
||||||
""
|
""
|
||||||
|
|
||||||
],
|
],
|
||||||
"description": "The starting base for an owl component"
|
"description": "The starting base for an owl component"
|
||||||
|
},
|
||||||
|
|
||||||
|
"Basic OWL Template": {
|
||||||
|
"prefix": "owltemplate",
|
||||||
|
"scope": "xml",
|
||||||
|
"body": [
|
||||||
|
"<?xml version=\"1.0\" encoding=\"UTF-8\" ?>",
|
||||||
|
"",
|
||||||
|
"<templates xml:space=\"preserve\">",
|
||||||
|
"",
|
||||||
|
" <t t-name=\"${2:${RELATIVE_FILEPATH/(.*[\\|\\/])??([a-zA-Z_]+)([\\|\\/]static[\\|\\/].*)/${2}/g}}.${3:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/g}}\">",
|
||||||
|
" ${3:<h1>Hello World</h1>}",
|
||||||
|
" </t>",
|
||||||
|
"",
|
||||||
|
"</templates>",
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"description": "Generate a basic OWL template XML file"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import * as vscode from 'vscode';
|
|
||||||
import { getSelectedText, showStatusMessage, hideStatusMessage } from './utils';
|
|
||||||
import { Search } from './search';
|
|
||||||
|
|
||||||
export class ComponentDefinitionProvider implements vscode.DefinitionProvider {
|
|
||||||
|
|
||||||
search: Search;
|
|
||||||
|
|
||||||
constructor(search: Search) {
|
|
||||||
this.search = search;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface implementation to provide definition when ctrl+click on Component
|
|
||||||
* tag in template.
|
|
||||||
*/
|
|
||||||
async provideDefinition(document: vscode.TextDocument, position: vscode.Position) {
|
|
||||||
const currentWord = getSelectedText(/<\/?[A-Z][a-zA-Z]+/, document, position);
|
|
||||||
if (!currentWord) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const componentName = currentWord.replace(/[\/<]/g, "").trim();
|
|
||||||
|
|
||||||
showStatusMessage(`Searching for component "${componentName}"`);
|
|
||||||
const result = await this.search.findComponent(componentName);
|
|
||||||
hideStatusMessage();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,19 @@
|
|||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { Search } from './search';
|
import { Search } from './search';
|
||||||
import { ComponentDefinitionProvider } from './definiton_providers';
|
|
||||||
import { OpenDirection } from './utils';
|
import { OpenDirection } from './utils';
|
||||||
|
import { OwlLanguageFeaturesProvider } from './language_features/language_features_provider';
|
||||||
|
|
||||||
export async function activate(context: vscode.ExtensionContext) {
|
export async function activate(context: vscode.ExtensionContext) {
|
||||||
const search = new Search();
|
const search = new Search();
|
||||||
|
|
||||||
context.subscriptions.push(vscode.commands.registerCommand('owl-vision.switch', () => search.switch()));
|
context.subscriptions.push(vscode.commands.registerCommand('owl-vision.switch', () => search.switch()));
|
||||||
context.subscriptions.push(vscode.commands.registerCommand('owl-vision.switch-besides', () => search.switch(OpenDirection.Besides)));
|
context.subscriptions.push(vscode.commands.registerCommand('owl-vision.switch-besides', () => search.switch(OpenDirection.Besides)));
|
||||||
context.subscriptions.push(vscode.commands.registerCommand('owl-vision.switch-below', () => search.switch(OpenDirection.Below)));
|
context.subscriptions.push(vscode.commands.registerCommand('owl-vision.switch-below', () => search.switch(OpenDirection.Below)));
|
||||||
context.subscriptions.push(vscode.commands.registerCommand('owl-vision.find-component', () => search.findComponentCommand()));
|
context.subscriptions.push(vscode.commands.registerCommand('owl-vision.find-component', () => search.findComponentCommand()));
|
||||||
context.subscriptions.push(vscode.commands.registerCommand('owl-vision.find-template', () => search.findTemplateCommand()));
|
context.subscriptions.push(vscode.commands.registerCommand('owl-vision.find-template', () => search.findTemplateCommand()));
|
||||||
|
|
||||||
const componentDefProvider = new ComponentDefinitionProvider(search);
|
const languageFeaturesProvider = new OwlLanguageFeaturesProvider(search);
|
||||||
context.subscriptions.push(vscode.languages.registerDefinitionProvider({ language: 'xml' }, componentDefProvider));
|
context.subscriptions.push(vscode.languages.registerCompletionItemProvider({ language: 'xml', scheme: 'file' }, languageFeaturesProvider, '.', '<'));
|
||||||
context.subscriptions.push(vscode.languages.registerDefinitionProvider({ language: 'javascript' }, componentDefProvider));
|
context.subscriptions.push(vscode.languages.registerDefinitionProvider({ language: 'xml', scheme: 'file' }, languageFeaturesProvider));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deactivate() { }
|
export function deactivate() { }
|
||||||
|
|||||||
@@ -0,0 +1,395 @@
|
|||||||
|
import { CompletionItemKind } from "vscode"
|
||||||
|
|
||||||
|
export const owlComponentAttributes = [
|
||||||
|
"t-if",
|
||||||
|
"t-else",
|
||||||
|
"t-elif",
|
||||||
|
"t-foreach",
|
||||||
|
"t-as",
|
||||||
|
"t-key",
|
||||||
|
"t-esc",
|
||||||
|
"t-out",
|
||||||
|
"t-props",
|
||||||
|
"t-set",
|
||||||
|
"t-value",
|
||||||
|
"t-portal",
|
||||||
|
"t-slot-scope",
|
||||||
|
"t-log",
|
||||||
|
].map(label => ({
|
||||||
|
label: label,
|
||||||
|
insertText: label + '=""',
|
||||||
|
kind: CompletionItemKind.Property,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const owlElementAttributes = [
|
||||||
|
"t-component",
|
||||||
|
"t-att",
|
||||||
|
"t-tag",
|
||||||
|
"t-model",
|
||||||
|
].map(label => ({
|
||||||
|
label: label,
|
||||||
|
insertText: label + '=""',
|
||||||
|
kind: CompletionItemKind.Property,
|
||||||
|
}));
|
||||||
|
|
||||||
|
owlElementAttributes.push(...owlComponentAttributes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
To generate this list, run the following snippet on https://developer.mozilla.org/fr/docs/Web/Events
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
const events = [...document.querySelectorAll(".section-content li a")]
|
||||||
|
.map(e => e.childNodes[0])
|
||||||
|
.filter(n => n.nodeType === 3 && n.textContent.toLowerCase() == n.textContent)
|
||||||
|
.map(n => "t-on-" + n.textContent.trim());
|
||||||
|
return JSON.stringify([...new Set(events)], null, 2);
|
||||||
|
}())
|
||||||
|
*/
|
||||||
|
export const events = [
|
||||||
|
"t-on-abort",
|
||||||
|
"t-on-ended",
|
||||||
|
"t-on-addtrack",
|
||||||
|
"t-on-change",
|
||||||
|
"t-on-removetrack",
|
||||||
|
"t-on-messageerror",
|
||||||
|
"t-on-message",
|
||||||
|
"t-on-animationcancel",
|
||||||
|
"t-on-animationend",
|
||||||
|
"t-on-animationiteration",
|
||||||
|
"t-on-animationstart",
|
||||||
|
"t-on-copy",
|
||||||
|
"t-on-cut",
|
||||||
|
"t-on-dragend",
|
||||||
|
"t-on-dragenter",
|
||||||
|
"t-on-dragleave",
|
||||||
|
"t-on-dragover",
|
||||||
|
"t-on-dragstart",
|
||||||
|
"t-on-drag",
|
||||||
|
"t-on-drop",
|
||||||
|
"t-on-fullscreenchange",
|
||||||
|
"t-on-fullscreenerror",
|
||||||
|
"t-on-gotpointercapture",
|
||||||
|
"t-on-keydown",
|
||||||
|
"t-on-keypress",
|
||||||
|
"t-on-keyup",
|
||||||
|
"t-on-lostpointercapture",
|
||||||
|
"t-on-paste",
|
||||||
|
"t-on-pointercancel",
|
||||||
|
"t-on-pointerdown",
|
||||||
|
"t-on-pointerenter",
|
||||||
|
"t-on-pointerleave",
|
||||||
|
"t-on-pointerlockchange",
|
||||||
|
"t-on-pointerlockerror",
|
||||||
|
"t-on-pointermove",
|
||||||
|
"t-on-pointerout",
|
||||||
|
"t-on-pointerover",
|
||||||
|
"t-on-pointerup",
|
||||||
|
"t-on-readystatechange",
|
||||||
|
"t-on-scroll",
|
||||||
|
"t-on-selectionchange",
|
||||||
|
"t-on-selectstart",
|
||||||
|
"t-on-touchcancel",
|
||||||
|
"t-on-touchend",
|
||||||
|
"t-on-touchmove",
|
||||||
|
"t-on-touchstart",
|
||||||
|
"t-on-transitioncancel",
|
||||||
|
"t-on-transitionend",
|
||||||
|
"t-on-transitionrun",
|
||||||
|
"t-on-transitionstart",
|
||||||
|
"t-on-visibilitychange",
|
||||||
|
"t-on-wheel",
|
||||||
|
"t-on-afterscriptexecute",
|
||||||
|
"t-on-auxclick",
|
||||||
|
"t-on-beforescriptexecute",
|
||||||
|
"t-on-blur",
|
||||||
|
"t-on-click",
|
||||||
|
"t-on-compositionend",
|
||||||
|
"t-on-compositionstart",
|
||||||
|
"t-on-compositionupdate",
|
||||||
|
"t-on-contextmenu",
|
||||||
|
"t-on-dblclick",
|
||||||
|
"t-on-error",
|
||||||
|
"t-on-focusin",
|
||||||
|
"t-on-focusout",
|
||||||
|
"t-on-focus",
|
||||||
|
"t-on-gesturechange",
|
||||||
|
"t-on-gestureend",
|
||||||
|
"t-on-gesturestart",
|
||||||
|
"t-on-mousedown",
|
||||||
|
"t-on-mouseenter",
|
||||||
|
"t-on-mouseleave",
|
||||||
|
"t-on-mousemove",
|
||||||
|
"t-on-mouseout",
|
||||||
|
"t-on-mouseover",
|
||||||
|
"t-on-mouseup",
|
||||||
|
"t-on-mousewheel",
|
||||||
|
"t-on-overflow",
|
||||||
|
"t-on-select",
|
||||||
|
"t-on-show",
|
||||||
|
"t-on-underflow",
|
||||||
|
"t-on-webkitmouseforcechanged",
|
||||||
|
"t-on-webkitmouseforcedown",
|
||||||
|
"t-on-webkitmouseforceup",
|
||||||
|
"t-on-webkitmouseforcewillbegin",
|
||||||
|
"t-on-open",
|
||||||
|
"t-on-loadend",
|
||||||
|
"t-on-loadstart",
|
||||||
|
"t-on-load",
|
||||||
|
"t-on-progress",
|
||||||
|
"t-on-webglcontextcreationerror",
|
||||||
|
"t-on-webglcontextlost",
|
||||||
|
"t-on-webglcontextrestored",
|
||||||
|
"t-on-toggle",
|
||||||
|
"t-on-cancel",
|
||||||
|
"t-on-close",
|
||||||
|
"t-on-beforeinput",
|
||||||
|
"t-on-input",
|
||||||
|
"t-on-formdata",
|
||||||
|
"t-on-reset",
|
||||||
|
"t-on-submit",
|
||||||
|
"t-on-invalid",
|
||||||
|
"t-on-search",
|
||||||
|
"t-on-canplaythrough",
|
||||||
|
"t-on-canplay",
|
||||||
|
"t-on-durationchange",
|
||||||
|
"t-on-emptied",
|
||||||
|
"t-on-loadeddata",
|
||||||
|
"t-on-loadedmetadata",
|
||||||
|
"t-on-pause",
|
||||||
|
"t-on-playing",
|
||||||
|
"t-on-play",
|
||||||
|
"t-on-ratechange",
|
||||||
|
"t-on-seeked",
|
||||||
|
"t-on-seeking",
|
||||||
|
"t-on-stalled",
|
||||||
|
"t-on-suspend",
|
||||||
|
"t-on-timeupdate",
|
||||||
|
"t-on-volumechange",
|
||||||
|
"t-on-waiting",
|
||||||
|
"t-on-slotchange",
|
||||||
|
"t-on-cuechange",
|
||||||
|
"t-on-enterpictureinpicture",
|
||||||
|
"t-on-leavepictureinpicture",
|
||||||
|
"t-on-versionchange",
|
||||||
|
"t-on-blocked",
|
||||||
|
"t-on-upgradeneeded",
|
||||||
|
"t-on-success",
|
||||||
|
"t-on-complete",
|
||||||
|
"t-on-devicechange",
|
||||||
|
"t-on-mute",
|
||||||
|
"t-on-unmute",
|
||||||
|
"t-on-merchantvalidation",
|
||||||
|
"t-on-paymentmethodchange",
|
||||||
|
"t-on-shippingaddresschange",
|
||||||
|
"t-on-shippingoptionchange",
|
||||||
|
"t-on-payerdetailchange",
|
||||||
|
"t-on-resourcetimingbufferfull",
|
||||||
|
"t-on-resize",
|
||||||
|
"t-on-bufferedamountlow",
|
||||||
|
"t-on-closing",
|
||||||
|
"t-on-tonechange",
|
||||||
|
"t-on-gatheringstatechange",
|
||||||
|
"t-on-selectedcandidatepairchange",
|
||||||
|
"t-on-statechange",
|
||||||
|
"t-on-addstream",
|
||||||
|
"t-on-connectionstatechange",
|
||||||
|
"t-on-datachannel",
|
||||||
|
"t-on-icecandidateerror",
|
||||||
|
"t-on-icecandidate",
|
||||||
|
"t-on-iceconnectionstatechange",
|
||||||
|
"t-on-icegatheringstatechange",
|
||||||
|
"t-on-negotiationneeded",
|
||||||
|
"t-on-removestream",
|
||||||
|
"t-on-signalingstatechange",
|
||||||
|
"t-on-track",
|
||||||
|
"t-on-audioprocess",
|
||||||
|
"t-on-activate",
|
||||||
|
"t-on-contentdelete",
|
||||||
|
"t-on-install",
|
||||||
|
"t-on-notificationclick",
|
||||||
|
"t-on-pushsubscriptionchange",
|
||||||
|
"t-on-push",
|
||||||
|
"t-on-connect",
|
||||||
|
"t-on-audioend",
|
||||||
|
"t-on-audiostart",
|
||||||
|
"t-on-end",
|
||||||
|
"t-on-nomatch",
|
||||||
|
"t-on-result",
|
||||||
|
"t-on-soundend",
|
||||||
|
"t-on-soundstart",
|
||||||
|
"t-on-speechend",
|
||||||
|
"t-on-speechstart",
|
||||||
|
"t-on-start",
|
||||||
|
"t-on-voiceschanged",
|
||||||
|
"t-on-boundary",
|
||||||
|
"t-on-mark",
|
||||||
|
"t-on-resume",
|
||||||
|
"t-on-unload",
|
||||||
|
"t-on-afterprint",
|
||||||
|
"t-on-appinstalled",
|
||||||
|
"t-on-beforeprint",
|
||||||
|
"t-on-beforeunload",
|
||||||
|
"t-on-devicemotion",
|
||||||
|
"t-on-deviceorientation",
|
||||||
|
"t-on-gamepadconnected",
|
||||||
|
"t-on-gamepaddisconnected",
|
||||||
|
"t-on-hashchange",
|
||||||
|
"t-on-languagechange",
|
||||||
|
"t-on-offline",
|
||||||
|
"t-on-online",
|
||||||
|
"t-on-orientationchange",
|
||||||
|
"t-on-pagehide",
|
||||||
|
"t-on-pageshow",
|
||||||
|
"t-on-popstate",
|
||||||
|
"t-on-rejectionhandled",
|
||||||
|
"t-on-storage",
|
||||||
|
"t-on-unhandledrejection",
|
||||||
|
"t-on-vrdisplayactivate",
|
||||||
|
"t-on-vrdisplayblur",
|
||||||
|
"t-on-vrdisplayconnect",
|
||||||
|
"t-on-vrdisplaydeactivate",
|
||||||
|
"t-on-vrdisplaydisconnect",
|
||||||
|
"t-on-vrdisplayfocus",
|
||||||
|
"t-on-vrdisplaypointerrestricted",
|
||||||
|
"t-on-vrdisplaypointerunrestricted",
|
||||||
|
"t-on-vrdisplaypresentchange",
|
||||||
|
"t-on-timeout",
|
||||||
|
"t-on-inputsourceschange",
|
||||||
|
"t-on-selectend",
|
||||||
|
"t-on-squeezeend",
|
||||||
|
"t-on-squeezestart",
|
||||||
|
"t-on-squeeze"
|
||||||
|
].map(label => ({
|
||||||
|
label: label,
|
||||||
|
insertText: label + '=""',
|
||||||
|
kind: CompletionItemKind.Property,
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
To generate the elements list, run the following snippet on https://developer.mozilla.org/en-US/docs/Web/HTML/Element
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
const names = [...document.querySelectorAll("section:not([aria-labelledby='obsolete_and_deprecated_elements']) td:nth-child(1)")]
|
||||||
|
.flatMap(n => n.innerText.split(","))
|
||||||
|
.map(n => n.trim().replace("<", "").replace(">", ""))
|
||||||
|
return JSON.stringify([...new Set(names)], null, 2);
|
||||||
|
})()
|
||||||
|
*/
|
||||||
|
export const elements = [
|
||||||
|
"t",
|
||||||
|
"link",
|
||||||
|
"meta",
|
||||||
|
"style",
|
||||||
|
"title",
|
||||||
|
"body",
|
||||||
|
"address",
|
||||||
|
"article",
|
||||||
|
"aside",
|
||||||
|
"footer",
|
||||||
|
"header",
|
||||||
|
"h1",
|
||||||
|
"h2",
|
||||||
|
"h3",
|
||||||
|
"h4",
|
||||||
|
"h5",
|
||||||
|
"h6",
|
||||||
|
"hgroup",
|
||||||
|
"main",
|
||||||
|
"nav",
|
||||||
|
"section",
|
||||||
|
"search",
|
||||||
|
"blockquote",
|
||||||
|
"dd",
|
||||||
|
"div",
|
||||||
|
"dl",
|
||||||
|
"dt",
|
||||||
|
"figcaption",
|
||||||
|
"figure",
|
||||||
|
"hr",
|
||||||
|
"li",
|
||||||
|
"menu",
|
||||||
|
"ol",
|
||||||
|
"p",
|
||||||
|
"pre",
|
||||||
|
"ul",
|
||||||
|
"a",
|
||||||
|
"abbr",
|
||||||
|
"b",
|
||||||
|
"bdi",
|
||||||
|
"bdo",
|
||||||
|
"br",
|
||||||
|
"cite",
|
||||||
|
"code",
|
||||||
|
"data",
|
||||||
|
"dfn",
|
||||||
|
"em",
|
||||||
|
"i",
|
||||||
|
"kbd",
|
||||||
|
"mark",
|
||||||
|
"q",
|
||||||
|
"rp",
|
||||||
|
"rt",
|
||||||
|
"ruby",
|
||||||
|
"s",
|
||||||
|
"samp",
|
||||||
|
"small",
|
||||||
|
"span",
|
||||||
|
"strong",
|
||||||
|
"sub",
|
||||||
|
"sup",
|
||||||
|
"time",
|
||||||
|
"u",
|
||||||
|
"var",
|
||||||
|
"wbr",
|
||||||
|
"area",
|
||||||
|
"audio",
|
||||||
|
"img",
|
||||||
|
"map",
|
||||||
|
"track",
|
||||||
|
"video",
|
||||||
|
"embed",
|
||||||
|
"iframe",
|
||||||
|
"object",
|
||||||
|
"picture",
|
||||||
|
"portal",
|
||||||
|
"source",
|
||||||
|
"svg",
|
||||||
|
"math",
|
||||||
|
"canvas",
|
||||||
|
"noscript",
|
||||||
|
"script",
|
||||||
|
"del",
|
||||||
|
"ins",
|
||||||
|
"caption",
|
||||||
|
"col",
|
||||||
|
"colgroup",
|
||||||
|
"table",
|
||||||
|
"tbody",
|
||||||
|
"td",
|
||||||
|
"tfoot",
|
||||||
|
"th",
|
||||||
|
"thead",
|
||||||
|
"tr",
|
||||||
|
"button",
|
||||||
|
"datalist",
|
||||||
|
"fieldset",
|
||||||
|
"form",
|
||||||
|
"input",
|
||||||
|
"label",
|
||||||
|
"legend",
|
||||||
|
"meter",
|
||||||
|
"optgroup",
|
||||||
|
"option",
|
||||||
|
"output",
|
||||||
|
"progress",
|
||||||
|
"select",
|
||||||
|
"textarea",
|
||||||
|
"details",
|
||||||
|
"dialog",
|
||||||
|
"summary",
|
||||||
|
].map(label => ({
|
||||||
|
label: label,
|
||||||
|
insertText: label,
|
||||||
|
kind: CompletionItemKind.Property,
|
||||||
|
}));
|
||||||
@@ -0,0 +1,422 @@
|
|||||||
|
import { CancellationToken, CompletionContext, CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList, DefinitionProvider, Location, Position, Range, TextDocument, TextDocumentContentProvider, Uri, commands, workspace } from "vscode";
|
||||||
|
import { Search } from "../search";
|
||||||
|
import { getSelectedText, hash, readFile } from "../utils";
|
||||||
|
import { elements, events, owlComponentAttributes, owlElementAttributes } from "./items";
|
||||||
|
import { ParseResultType, ParseResult, parse, getNodePath, parseXml } from "./parser";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commands return basic js object which needs to be converted
|
||||||
|
* to actual CompletionItem instances, this methods streamlines
|
||||||
|
* this process.
|
||||||
|
*/
|
||||||
|
function mapCompletionItems(items: any): CompletionItem[] {
|
||||||
|
return items.map((i: any) => {
|
||||||
|
const item = new CompletionItem(i.label, i.kind);
|
||||||
|
item.sortText = i.sortText;
|
||||||
|
item.detail = i.detail;
|
||||||
|
item.filterText = i.filterText;
|
||||||
|
item.insertText = i.insertText?.startsWith?.(".") ? i.insertText.substring(1) : i.insertText;
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterComponentItems(items: CompletionItem[], excludedLabels: string[] = []): CompletionItem[] {
|
||||||
|
return items.filter((item) => {
|
||||||
|
return !excludedLabels.includes(item.label.toString()) && [
|
||||||
|
CompletionItemKind.Field,
|
||||||
|
CompletionItemKind.Method,
|
||||||
|
CompletionItemKind.Variable,
|
||||||
|
CompletionItemKind.Property,
|
||||||
|
].includes(item.kind as number);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds "this." in front of the expression if needed and increments
|
||||||
|
* the expression offset accordingly.
|
||||||
|
*/
|
||||||
|
function contextualize(properties: string[], expression: string, expressionOffset = 0) {
|
||||||
|
const match = expression.match(/^([a-zA-Z_]+)\b/);
|
||||||
|
if (!expression.startsWith("this.") && ((match && properties.includes(match[1])) || expression.match(/^\s*$/))) {
|
||||||
|
expression = "this." + expression;
|
||||||
|
expressionOffset += 5;
|
||||||
|
}
|
||||||
|
return { expression, expressionOffset };
|
||||||
|
};
|
||||||
|
|
||||||
|
const Commands = {
|
||||||
|
Completion: "vscode.executeCompletionItemProvider",
|
||||||
|
Definition: "vscode.executeDefinitionProvider",
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OwlLanguageFeaturesProvider implements CompletionItemProvider, TextDocumentContentProvider, DefinitionProvider {
|
||||||
|
|
||||||
|
virtualDocuments = new Map();
|
||||||
|
componentProperties = new Map();
|
||||||
|
search: Search;
|
||||||
|
|
||||||
|
constructor(search: Search) {
|
||||||
|
this.search = search;
|
||||||
|
workspace.registerTextDocumentContentProvider("owl", this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TextDocumentContentProvider interface implementation to provide
|
||||||
|
* virtual documents to vscode commands.
|
||||||
|
*/
|
||||||
|
async provideTextDocumentContent(uri: Uri) {
|
||||||
|
const id = uri.toString(true);
|
||||||
|
return this.virtualDocuments.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DefinitionProvider interface implementation.
|
||||||
|
*
|
||||||
|
* - If the target is a js expression, will try to find the definition
|
||||||
|
* inside the current component.
|
||||||
|
* - If the target is a component element, will try to find the definition
|
||||||
|
* of the component.
|
||||||
|
*/
|
||||||
|
async provideDefinition(document: TextDocument, position: Position) {
|
||||||
|
let offset = document.offsetAt(position);
|
||||||
|
const documentText = document.getText();
|
||||||
|
const parseResult = await parse(documentText, offset);
|
||||||
|
|
||||||
|
if (parseResult.type === ParseResultType.Expression) {
|
||||||
|
const component = await this.search.getCurrentComponent();
|
||||||
|
if (!component) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { xmlDocument, xmlNode } = parseXml(documentText, offset);
|
||||||
|
const componentText = await readFile(component.uri);
|
||||||
|
const virtualDocument = await this.getVirtualJsDocument(document.uri, component.componentName, componentText, xmlDocument, xmlNode, parseResult);
|
||||||
|
|
||||||
|
const definitions: any = await this.executeCommand(
|
||||||
|
Commands.Definition,
|
||||||
|
document.uri,
|
||||||
|
virtualDocument.content,
|
||||||
|
virtualDocument.offset
|
||||||
|
);
|
||||||
|
|
||||||
|
if (definitions.length > 0) {
|
||||||
|
const selectionRange = definitions[0].targetSelectionRange;
|
||||||
|
const range = new Range(
|
||||||
|
new Position(selectionRange.start.line, selectionRange.start.character),
|
||||||
|
new Position(selectionRange.end.line, selectionRange.end.character),
|
||||||
|
)
|
||||||
|
return new Location(component.uri, range);
|
||||||
|
}
|
||||||
|
} else if (parseResult.type === ParseResultType.Attribute) {
|
||||||
|
const { xmlNode } = parseXml(documentText, offset);
|
||||||
|
|
||||||
|
const childComponent = await this.search.findComponent(xmlNode.name);
|
||||||
|
if (!childComponent) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const modifiersRegex = new RegExp([
|
||||||
|
"\\.bind",
|
||||||
|
"\\.stop",
|
||||||
|
"\\.prevent",
|
||||||
|
"\\.self",
|
||||||
|
"\\.capture",
|
||||||
|
"\\.sythetic",
|
||||||
|
].join("|"), "g");
|
||||||
|
|
||||||
|
let attributeName = getSelectedText(/\b[a-zA-Z0-9_\-.]+\b/, document, position)
|
||||||
|
attributeName = attributeName?.replace(modifiersRegex, "") ?? "";
|
||||||
|
|
||||||
|
const componentText = await readFile(childComponent.uri);
|
||||||
|
const content = `${componentText}\n${xmlNode.name}.props.${attributeName}`;
|
||||||
|
const definitions: any = await this.executeCommand(Commands.Definition, document.uri, content);
|
||||||
|
|
||||||
|
if (definitions.length > 0) {
|
||||||
|
const selectionRange = definitions[0].targetSelectionRange;
|
||||||
|
const range = new Range(
|
||||||
|
new Position(selectionRange.start.line, selectionRange.start.character),
|
||||||
|
new Position(selectionRange.end.line, selectionRange.end.character),
|
||||||
|
)
|
||||||
|
return new Location(childComponent.uri, range);
|
||||||
|
}
|
||||||
|
} else if (parseResult.type === ParseResultType.Element) {
|
||||||
|
const currentWord = getSelectedText(/<\/?[A-Z][a-zA-Z]+/, document, position);
|
||||||
|
if (!currentWord) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const componentName = currentWord.replace(/[\/<]/g, "").trim();
|
||||||
|
return await this.search.findComponent(componentName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CompletionItemProvider interface implementation
|
||||||
|
*
|
||||||
|
* See {@link provideElementItems}, {@link provideAttributeItems} and {@link provideExpressionItems}
|
||||||
|
* for further details.
|
||||||
|
*/
|
||||||
|
async provideCompletionItems(
|
||||||
|
document: TextDocument,
|
||||||
|
position: Position,
|
||||||
|
token: CancellationToken,
|
||||||
|
context: CompletionContext
|
||||||
|
): Promise<CompletionItem[]> {
|
||||||
|
const component = await this.search.getCurrentComponent();
|
||||||
|
if (!component || token.isCancellationRequested) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const offset = document.offsetAt(position);
|
||||||
|
const documentText = document.getText();
|
||||||
|
|
||||||
|
const parseResult = await parse(documentText, offset);
|
||||||
|
const { xmlDocument, xmlNode } = parseXml(documentText, offset);
|
||||||
|
|
||||||
|
if (parseResult.type === ParseResultType.Expression) {
|
||||||
|
return this.provideExpressionItems(document.uri, component.uri, component.componentName, xmlDocument, xmlNode, parseResult);
|
||||||
|
} else if (parseResult.type === ParseResultType.Attribute) {
|
||||||
|
return this.provideAttributeItems(document.uri, xmlNode);
|
||||||
|
} else if (parseResult.type === ParseResultType.Element) {
|
||||||
|
return this.provideElementItems(document.uri, component.uri, component.componentName, parseResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the completion items for attributes.
|
||||||
|
* - Returns props if the element is a component
|
||||||
|
* - Returns the owl directives based on the element type
|
||||||
|
*/
|
||||||
|
private async provideAttributeItems(
|
||||||
|
documentUri: Uri,
|
||||||
|
xmlNode: any,
|
||||||
|
): Promise<CompletionItem[]> {
|
||||||
|
if (!xmlNode || xmlNode.name === xmlNode.name.toLowerCase()) {
|
||||||
|
return [...owlElementAttributes, ...events];
|
||||||
|
}
|
||||||
|
|
||||||
|
const childComponent = await this.search.findComponent(xmlNode.name);
|
||||||
|
if (!childComponent) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const componentText = await readFile(childComponent.uri);
|
||||||
|
const content = `${componentText}\n${xmlNode.name}.props.`;
|
||||||
|
const list = await this.executeCommand(Commands.Completion, documentUri, content) as CompletionList;
|
||||||
|
|
||||||
|
const modifiersRegex = new RegExp([
|
||||||
|
"\\.bind",
|
||||||
|
"\\.stop",
|
||||||
|
"\\.prevent",
|
||||||
|
"\\.self",
|
||||||
|
"\\.capture",
|
||||||
|
"\\.sythetic",
|
||||||
|
].join("|"), "g");
|
||||||
|
|
||||||
|
const excludedAttrs = [
|
||||||
|
"slots",
|
||||||
|
...Object.keys(xmlNode.attr).map(attr => attr.replace(modifiersRegex, ""))
|
||||||
|
];
|
||||||
|
|
||||||
|
return mapCompletionItems(filterComponentItems([
|
||||||
|
...owlComponentAttributes,
|
||||||
|
...list.items
|
||||||
|
], excludedAttrs));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the completion items for elements, this includes
|
||||||
|
* components, "t" and html elements.
|
||||||
|
*/
|
||||||
|
private async provideElementItems(
|
||||||
|
documentUri: Uri,
|
||||||
|
componentUri: Uri,
|
||||||
|
componentName: string,
|
||||||
|
parseResult: any,
|
||||||
|
): Promise<CompletionItem[]> {
|
||||||
|
const componentText = await readFile(componentUri);
|
||||||
|
|
||||||
|
const content = `${componentText}\n${componentName}.components.${parseResult.expression}`;
|
||||||
|
const list = await this.executeCommand(Commands.Completion, documentUri, content) as CompletionList;
|
||||||
|
|
||||||
|
return mapCompletionItems([
|
||||||
|
...elements,
|
||||||
|
...filterComponentItems(list.items),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the completion items for a js expression
|
||||||
|
*/
|
||||||
|
private async provideExpressionItems(
|
||||||
|
documentUri: Uri,
|
||||||
|
componentUri: Uri,
|
||||||
|
componentName: string,
|
||||||
|
xmlDocument: any,
|
||||||
|
xmlNode: any,
|
||||||
|
parseResult: ParseResult,
|
||||||
|
): Promise<CompletionItem[]> {
|
||||||
|
let { attributeName } = parseResult;
|
||||||
|
|
||||||
|
const dynamicAttributeRegex = new RegExp([
|
||||||
|
"t-if",
|
||||||
|
"t-elif",
|
||||||
|
"t-foreach",
|
||||||
|
"t-as",
|
||||||
|
"t-key",
|
||||||
|
"t-esc",
|
||||||
|
"t-out",
|
||||||
|
"t-props",
|
||||||
|
"t-component",
|
||||||
|
"t-set",
|
||||||
|
"t-value",
|
||||||
|
"t-portal",
|
||||||
|
"t-slot-scope",
|
||||||
|
"t-att",
|
||||||
|
"t-tag",
|
||||||
|
"t-log",
|
||||||
|
"t-model",
|
||||||
|
"t-att-[a-z_:.-]+",
|
||||||
|
"t-on-[a-z_:.-]+"
|
||||||
|
].join("|"));
|
||||||
|
|
||||||
|
if (xmlNode.name === xmlNode.name.toLowerCase() && !dynamicAttributeRegex.test(attributeName)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const componentText = await readFile(componentUri);
|
||||||
|
const virtualDocument = await this.getVirtualJsDocument(documentUri, componentName, componentText, xmlDocument, xmlNode, parseResult);
|
||||||
|
|
||||||
|
const completionList = await this.executeCommand(
|
||||||
|
Commands.Completion,
|
||||||
|
documentUri,
|
||||||
|
virtualDocument.content,
|
||||||
|
virtualDocument.offset
|
||||||
|
) as CompletionList;
|
||||||
|
|
||||||
|
let items = filterComponentItems(completionList.items, ["__VIRTUAL__", "setup"]);
|
||||||
|
|
||||||
|
if (!/\bthis\./.test(parseResult.expression)) {
|
||||||
|
items = items.map(item => {
|
||||||
|
let insertText = item.insertText as string;
|
||||||
|
if (/\bthis\./.test(insertText)) {
|
||||||
|
item.insertText = insertText.replace(/\bthis\./, "");
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapCompletionItems([...items]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a virtual document to provide the appropriate
|
||||||
|
* completion items for a parsed js expression.
|
||||||
|
*
|
||||||
|
* This method:
|
||||||
|
* - Adds default owl variables such as env and props
|
||||||
|
* - Adds mocks for frequently used Owl imports (which cannot be resolved using commands)
|
||||||
|
* - Adds local variables generated from Owl xml directives such as t-for or t-set
|
||||||
|
* - Will try to add a "this." in front of the js expression if it was omitted so it can
|
||||||
|
* be understood by vscode typescript server.
|
||||||
|
*
|
||||||
|
* The expression offset is also modified accordingly.
|
||||||
|
*/
|
||||||
|
async getVirtualJsDocument(
|
||||||
|
documentUri: Uri,
|
||||||
|
componentName: string,
|
||||||
|
componentText: string,
|
||||||
|
xmlDocument: any,
|
||||||
|
xmlNode: any,
|
||||||
|
parseResult: ParseResult,
|
||||||
|
) {
|
||||||
|
const properties = await this.getComponentProperties(documentUri, componentName, componentText);
|
||||||
|
|
||||||
|
// As imports do not work, use mocks for frequently used owl functions.
|
||||||
|
let importReplacements = workspace.getConfiguration().get(`owl-vision.autocomplete-mocks`);
|
||||||
|
|
||||||
|
// As imports do not work, manually add "env" and "props"
|
||||||
|
// to the current component instance.
|
||||||
|
const localVariables = [
|
||||||
|
"let env = {};",
|
||||||
|
"this.env = env;",
|
||||||
|
`let props = ${componentName}.props;`,
|
||||||
|
`this.props = props;`,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Adds local variables generated based on the template
|
||||||
|
const path = getNodePath(xmlDocument, xmlNode);
|
||||||
|
for (const node of path) {
|
||||||
|
if (node.attr["t-foreach"]) {
|
||||||
|
let array = contextualize(properties, node.attr["t-foreach"]).expression;
|
||||||
|
localVariables.push(`const ${node.attr["t-as"]} = ${array}[0];`);
|
||||||
|
localVariables.push(`const ${node.attr["t-as"]}_index = 0;`);
|
||||||
|
localVariables.push(`const ${node.attr["t-as"]}_first = ${array}[0];`);
|
||||||
|
localVariables.push(`const ${node.attr["t-as"]}_last = ${array}.at(-1);`);
|
||||||
|
localVariables.push(`const ${node.attr["t-as"]}_value = {};`);
|
||||||
|
} else if (node.attr["t-set"]) {
|
||||||
|
localVariables.push(`const ${node.attr["t-set"]} = ${node.attr["t-value"]};`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { expression, expressionOffset } = contextualize(properties, parseResult.expression, parseResult.expressionOffset);
|
||||||
|
|
||||||
|
return {
|
||||||
|
offset: expressionOffset,
|
||||||
|
content: `${componentText}
|
||||||
|
${importReplacements}
|
||||||
|
class __VIRTUAL__ extends ${componentName} { __VIRTUAL__() {
|
||||||
|
${localVariables.join("\n")}
|
||||||
|
${expression} }}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of properties for a given component class.
|
||||||
|
* The result in cached in `componentProperties`.
|
||||||
|
*
|
||||||
|
* @param documentUri
|
||||||
|
* @param componentName
|
||||||
|
* @param componentText
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getComponentProperties(
|
||||||
|
documentUri: Uri,
|
||||||
|
componentName: string,
|
||||||
|
componentText: string,
|
||||||
|
): Promise<string[]> {
|
||||||
|
const check = hash(componentText);
|
||||||
|
|
||||||
|
let cached = this.componentProperties.get(componentName);
|
||||||
|
if (!cached || cached.check !== check) {
|
||||||
|
const contextExpression = `${componentText}\nclass __VIRTUAL__ extends ${componentName} { __VIRTUAL__() { \nthis. }}`;
|
||||||
|
const contextItemsCompletion = await this.executeCommand(Commands.Completion, documentUri, contextExpression, 5) as CompletionList;
|
||||||
|
|
||||||
|
const items = filterComponentItems(contextItemsCompletion.items, ["__VIRTUAL__", "setup"]).map(item => item.label);
|
||||||
|
|
||||||
|
cached = { check, items };
|
||||||
|
this.componentProperties.set(componentName, cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cached.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async executeCommand(commandId: string, uri: Uri, content: string, offset: any = undefined) {
|
||||||
|
const lines = content.split(/\r\n|\r|\n/);
|
||||||
|
const _offset = offset !== undefined ? offset : (lines.at(-1)?.length ?? 0);
|
||||||
|
const position = new Position(lines.length - 1, _offset);
|
||||||
|
|
||||||
|
const originalUri = uri.toString(true);
|
||||||
|
const hashValue = hash(content);
|
||||||
|
|
||||||
|
const id = `owl://js/${originalUri}_${hashValue}.js`;
|
||||||
|
this.virtualDocuments.set(id, content);
|
||||||
|
|
||||||
|
return await commands.executeCommand(
|
||||||
|
commandId,
|
||||||
|
Uri.parse(`owl://js/${encodeURIComponent(originalUri)}_${hashValue}.js`),
|
||||||
|
position
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import { XmlDocument, XmlElement, XmlNode } from "xmldoc";
|
||||||
|
|
||||||
|
export enum ParseResultType {
|
||||||
|
Expression,
|
||||||
|
Attribute,
|
||||||
|
Element
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParseResult {
|
||||||
|
type: ParseResultType;
|
||||||
|
expression: string;
|
||||||
|
expressionOffset: number;
|
||||||
|
attributeName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will parse the document to find the selected expression based on an offset.
|
||||||
|
* The result can be on of three types:
|
||||||
|
*
|
||||||
|
* Element: The offset is on a element tag name, the expression is the current
|
||||||
|
* tagname or and empty string if it's just a opening tag.
|
||||||
|
*
|
||||||
|
* Attribute: The offset is inside the element but not in an attribute value,
|
||||||
|
* the expression is the current attribute name if any.
|
||||||
|
*
|
||||||
|
* Expression: The offset is inside an attribute value, the expression is the value.
|
||||||
|
*/
|
||||||
|
export async function parse(
|
||||||
|
documentText: string,
|
||||||
|
offset: number,
|
||||||
|
): Promise<ParseResult> {
|
||||||
|
|
||||||
|
// Check if the offset is preceded by "<xyz", if true returns a type Element
|
||||||
|
// with the current name.
|
||||||
|
const elementMatch = documentText.substring(0, offset).match(/<([a-zA-Z\-._]*)$/);
|
||||||
|
if (elementMatch) {
|
||||||
|
return {
|
||||||
|
type: ParseResultType.Element,
|
||||||
|
expression: elementMatch[1] || "",
|
||||||
|
expressionOffset: elementMatch[1].length || 0,
|
||||||
|
attributeName: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
value: expression,
|
||||||
|
offset: expressionOffset,
|
||||||
|
from,
|
||||||
|
} = getSection(documentText, offset, '="', '"');
|
||||||
|
|
||||||
|
// If the expression contains '"', it means we aren't inside an attribute
|
||||||
|
// value.
|
||||||
|
if (expression.includes('"')) {
|
||||||
|
const attributeMatch = documentText.substring(0, offset).match(/\s([a-zA-Z\-._]*)$/);
|
||||||
|
if (attributeMatch) {
|
||||||
|
return {
|
||||||
|
type: ParseResultType.Attribute,
|
||||||
|
expression: attributeMatch[1] || "",
|
||||||
|
expressionOffset: attributeMatch[1].length || 0,
|
||||||
|
attributeName: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let attributeName = "";
|
||||||
|
let i = from - 2;
|
||||||
|
while (/\S/.test(documentText[i])) {
|
||||||
|
attributeName = documentText[i] + attributeName;
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: ParseResultType.Expression,
|
||||||
|
expression,
|
||||||
|
expressionOffset,
|
||||||
|
attributeName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSection(text: string, offset: number, prefix: string, postfix: string) {
|
||||||
|
const beforeText = text.substring(0, offset);
|
||||||
|
let from = beforeText.lastIndexOf(prefix);
|
||||||
|
const afterText = text.substring(offset);
|
||||||
|
const to = beforeText.length + afterText.indexOf(postfix);
|
||||||
|
|
||||||
|
from = from + (prefix.length);
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: text.substring(from, to),
|
||||||
|
offset: offset - from,
|
||||||
|
from: from,
|
||||||
|
to,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns 2 xml nodes:
|
||||||
|
* xmlNode: Tries to create the current element based on a string offset,
|
||||||
|
* even if the node is invalid.
|
||||||
|
* xmlDocument: The document root element, only works if the document is
|
||||||
|
* valid xml.
|
||||||
|
*/
|
||||||
|
export function parseXml(text: string, offset: number): any {
|
||||||
|
let xmlDocument = undefined;
|
||||||
|
try {
|
||||||
|
xmlDocument = new XmlDocument(text);
|
||||||
|
} catch (error) { }
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
while (text[offset + i] !== "<") {
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
|
||||||
|
let node = "";
|
||||||
|
while (text[offset + i] !== ">" || text[offset + i - 1] === "=") {
|
||||||
|
node += text[offset + i];
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
let xmlNode = undefined;
|
||||||
|
try {
|
||||||
|
xmlNode = new XmlDocument(`${node}${node.endsWith("/") ? '' : '/'}>`);
|
||||||
|
} catch (error) { }
|
||||||
|
|
||||||
|
return { xmlDocument, xmlNode };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array representing the elements order from the document's
|
||||||
|
* root to the specified element.
|
||||||
|
*/
|
||||||
|
export function getNodePath(xmlDocument: XmlDocument, xmlNode: any): Array<XmlElement> {
|
||||||
|
let path: Array<XmlElement> = [];
|
||||||
|
|
||||||
|
const traverse = (node: XmlElement, currentPath: Array<XmlElement>) => {
|
||||||
|
if (node.name === xmlNode.name && JSON.stringify(node.attr) === JSON.stringify(xmlNode.attr)) {
|
||||||
|
path = currentPath;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.children) {
|
||||||
|
for (const child of node.children) {
|
||||||
|
if (child instanceof XmlElement && child.name) {
|
||||||
|
traverse(child, currentPath.concat(child));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
traverse(xmlDocument, []);
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
@@ -94,6 +94,31 @@ export class Search {
|
|||||||
return await this.find(templateName, query, "xml");
|
return await this.find(templateName, query, "xml");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getCurrentComponent(): Promise<any | undefined> {
|
||||||
|
if (!this.currentDocument) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = this.currentDocument.getText();
|
||||||
|
const templateName = this.getTemplateName(text, false);
|
||||||
|
|
||||||
|
if (templateName) {
|
||||||
|
const component = await this.findComponentFromTemplateName(templateName);
|
||||||
|
|
||||||
|
if (component) {
|
||||||
|
const componentFile = await workspace.fs.readFile(component.uri);
|
||||||
|
const componentText = Buffer.from(componentFile).toString('utf8');
|
||||||
|
const componentName = this.getComponentName(componentText, templateName);
|
||||||
|
|
||||||
|
return {
|
||||||
|
uri: component.uri,
|
||||||
|
templateName,
|
||||||
|
componentName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private findComponentFromTemplateName(templateName: string): Promise<Location | undefined> {
|
private findComponentFromTemplateName(templateName: string): Promise<Location | undefined> {
|
||||||
const query = this.buildQuery(`template\\s*=\\s*["']`, templateName, `["']`);
|
const query = this.buildQuery(`template\\s*=\\s*["']`, templateName, `["']`);
|
||||||
return this.find(templateName, query, "js");
|
return this.find(templateName, query, "js");
|
||||||
@@ -107,6 +132,27 @@ export class Search {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getComponentName(str: string, templateName: string): string {
|
||||||
|
const templateNameRegex = new RegExp(`template\\s*=\\s*["'](${templateName})["']`, 'g');
|
||||||
|
const templateIndex = [...str.matchAll(templateNameRegex)][0]?.index ?? 0;
|
||||||
|
|
||||||
|
const matches = [...str.matchAll(new RegExp(`class\\s+([A-Za-z_]+)\\sextends\\s+[A-Za-z_]+`, 'g'))];
|
||||||
|
let result = "";
|
||||||
|
let currentIndex = -1;
|
||||||
|
for (const match of matches) {
|
||||||
|
if (match.index > templateIndex) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match.index > currentIndex) {
|
||||||
|
result = match[1];
|
||||||
|
currentIndex = match.index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
public async find(
|
public async find(
|
||||||
name: string,
|
name: string,
|
||||||
searchQuery: string,
|
searchQuery: string,
|
||||||
|
|||||||
@@ -102,3 +102,20 @@ export async function showResult(result: vscode.Location, openDirection: OpenDir
|
|||||||
editor.revealRange(result.range);
|
editor.revealRange(result.range);
|
||||||
editor.selection = new vscode.Selection(result.range.start, result.range.end);
|
editor.selection = new vscode.Selection(result.range.start, result.range.end);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function readFile(uri: vscode.Uri): Promise<string> {
|
||||||
|
const data = await vscode.workspace.fs.readFile(uri);
|
||||||
|
return Buffer.from(data).toString('utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hash(str: string) {
|
||||||
|
var hash = 0,
|
||||||
|
i, chr;
|
||||||
|
if (str.length === 0) return hash;
|
||||||
|
for (i = 0; i < str.length; i++) {
|
||||||
|
chr = str.charCodeAt(i);
|
||||||
|
hash = ((hash << 5) - hash) + chr;
|
||||||
|
hash |= 0;
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|||||||
@@ -160,7 +160,7 @@
|
|||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{
|
||||||
"contentName": "meta.embedded.block.javascript string.quoted.double.xml",
|
"contentName": "meta.embedded.block.javascript string.quoted.double.xml",
|
||||||
"begin": "(\\s*)(t-if|t-else|t-elif|t-foreach|t-as|t-key|t-esc|t-out|t-props|t-component|t-set|t-value|t-portal|t-slot-scope|t-att-[a-z_:.-]+|t-on-[a-z_:.-]+)(=)(\")",
|
"begin": "(\\s*)(t-if|t-elif|t-foreach|t-as|t-key|t-esc|t-out|t-props|t-component|t-set|t-value|t-portal|t-slot-scope|t-att|t-tag|t-log|t-model|t-att-[a-z_:.-]+|t-on-[a-z_:.-]+)(=)(\")",
|
||||||
"beginCaptures": {
|
"beginCaptures": {
|
||||||
"2": {
|
"2": {
|
||||||
"name": "entity.other.attribute-name.localname.xml owl.attribute owl.attribute.dynamic"
|
"name": "entity.other.attribute-name.localname.xml owl.attribute owl.attribute.dynamic"
|
||||||
@@ -183,7 +183,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"contentName": "meta.embedded.block.javascript string.quoted.single.xml",
|
"contentName": "meta.embedded.block.javascript string.quoted.single.xml",
|
||||||
"begin": "(\\s*)(t-if|t-else|t-elif|t-foreach|t-as|t-key|t-esc|t-out|t-props|t-component|t-set|t-value|t-portal|t-slot-scope|t-att-[a-z_:.-]+|t-on-[a-z_:.-]+)(=)(')",
|
"begin": "(\\s*)(t-if|t-elif|t-foreach|t-as|t-key|t-esc|t-out|t-props|t-component|t-set|t-value|t-portal|t-slot-scope|t-att|t-tag|t-log|t-model|t-att-[a-z_:.-]+|t-on-[a-z_:.-]+)(=)(')",
|
||||||
"beginCaptures": {
|
"beginCaptures": {
|
||||||
"2": {
|
"2": {
|
||||||
"name": "entity.other.attribute-name.localname.xml owl.attribute owl.attribute.dynamic"
|
"name": "entity.other.attribute-name.localname.xml owl.attribute owl.attribute.dynamic"
|
||||||
@@ -210,7 +210,7 @@
|
|||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{
|
||||||
"contentName": "string.quoted.double.xml",
|
"contentName": "string.quoted.double.xml",
|
||||||
"begin": "(\\s*)(t-name|t-ref|t-set-slot|t-model|t-inherit|t-inherit-mode|t-translation)(=)(\")",
|
"begin": "(\\s*)(t-name|t-else|t-ref|t-set-slot|t-inherit|t-inherit-mode|t-translation|t-debug)(=)(\")",
|
||||||
"beginCaptures": {
|
"beginCaptures": {
|
||||||
"2": {
|
"2": {
|
||||||
"name": "entity.other.attribute-name.localname.xml owl.attribute owl.attribute.static"
|
"name": "entity.other.attribute-name.localname.xml owl.attribute owl.attribute.static"
|
||||||
@@ -229,7 +229,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"contentName": "string.quoted.single.xml",
|
"contentName": "string.quoted.single.xml",
|
||||||
"begin": "(\\s*)(t-name|t-ref|t-set-slot|t-model|t-inherit|t-inherit-mode|t-translation)(=)(')",
|
"begin": "(\\s*)(t-name|t-else|t-ref|t-set-slot|t-inherit|t-inherit-mode|t-translation|t-debug)(=)(')",
|
||||||
"beginCaptures": {
|
"beginCaptures": {
|
||||||
"2": {
|
"2": {
|
||||||
"name": "entity.other.attribute-name.localname.xml owl.attribute owl.attribute.static"
|
"name": "entity.other.attribute-name.localname.xml owl.attribute owl.attribute.static"
|
||||||
|
|||||||
Reference in New Issue
Block a user