diff --git a/demo/App.vue b/demo/App.vue
index 0b39a33..5cfaa05 100644
--- a/demo/App.vue
+++ b/demo/App.vue
@@ -1,5 +1,5 @@
-
+
Vue Live renders vue code in the browser
vue-live is a VueJs component. It renders the code passed in prop
@@ -35,9 +35,7 @@
Pure JavaScript code
Or if you prefer to, use the new Vue() format
-
- Extra dependencies
-
+ Extra dependencies
Use the requires prop to make libraries and packages available in
the browser
@@ -60,7 +58,7 @@
/>
Default Layout
-
+
Custom Layout
@@ -84,11 +82,19 @@
Separate components for Editor and Preview
-
+ (error = e)"
+ @success="error = undefined"
+ />
-
+
Edit the code here
-
+
@@ -128,6 +134,7 @@ export default {
realjsx,
separateCode: codeSfc,
openExamples: false,
+ error: undefined,
};
},
methods: {
diff --git a/package-lock.json b/package-lock.json
index 08b4721..de420fd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6625,9 +6625,9 @@
},
"dependencies": {
"acorn": {
- "version": "6.4.1",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz",
- "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA=="
+ "version": "6.4.2",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz",
+ "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ=="
},
"os-homedir": {
"version": "2.0.0",
@@ -19365,29 +19365,29 @@
"dev": true
},
"vue-inbrowser-compiler": {
- "version": "4.32.1",
- "resolved": "https://registry.npmjs.org/vue-inbrowser-compiler/-/vue-inbrowser-compiler-4.32.1.tgz",
- "integrity": "sha512-eHNJ6fenrTISxS2t540eB6K7O4AcZX7x73ZHHkK78sGAFeRDaxB17gEqtPPY2mgSi124gEy1I0MzFo+cHEJNSA==",
+ "version": "4.33.4",
+ "resolved": "https://registry.npmjs.org/vue-inbrowser-compiler/-/vue-inbrowser-compiler-4.33.4.tgz",
+ "integrity": "sha512-7CmoZaXMLQlA8DH3849OOeuYIhHTC0jHYNsrCxN/nBp/1wD+4e6tAedE5yqcy0zw8qfXRmK5Sl/TZW4GZA8lzg==",
"requires": {
"acorn": "^6.1.1",
"acorn-jsx": "^5.0.1",
"buble": "^0.19.7",
"camelcase": "^5.3.1",
- "vue-inbrowser-compiler-utils": "^4.32.1",
+ "vue-inbrowser-compiler-utils": "^4.33.4",
"walkes": "^0.2.1"
},
"dependencies": {
"acorn": {
- "version": "6.4.1",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz",
- "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA=="
+ "version": "6.4.2",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz",
+ "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ=="
}
}
},
"vue-inbrowser-compiler-utils": {
- "version": "4.32.1",
- "resolved": "https://registry.npmjs.org/vue-inbrowser-compiler-utils/-/vue-inbrowser-compiler-utils-4.32.1.tgz",
- "integrity": "sha512-IL8rBV3lCyHErqD8sBdQhWz3zJ/wLzG6JfoSzZ3K6HShS5QqIQfJN0GESvzIos6EGvmtByEf4TTJnjm12b51VQ==",
+ "version": "4.33.4",
+ "resolved": "https://registry.npmjs.org/vue-inbrowser-compiler-utils/-/vue-inbrowser-compiler-utils-4.33.4.tgz",
+ "integrity": "sha512-8/YvqKy0s7UUUdR8eHLwbuAEb3qs5SfM4Og21E9scKxAwfFBw20YJCLnXB/uEbYsGGpuMg/oY0P7lehfBhm1Tw==",
"requires": {
"camelcase": "^5.3.1"
}
diff --git a/package.json b/package.json
index 2e0d282..e6a5e11 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,7 @@
"hash-sum": "^2.0.0",
"prismjs": "^1.21.0",
"recast": "^0.19.1",
- "vue-inbrowser-compiler": "^4.32.1",
+ "vue-inbrowser-compiler": "^4.33.4",
"vue-prism-editor": "^1.2.2",
"vue-template-compiler": "^2.6.12"
},
diff --git a/src/Editor.vue b/src/Editor.vue
index e31e593..6776030 100644
--- a/src/Editor.vue
+++ b/src/Editor.vue
@@ -9,55 +9,14 @@
+
+
diff --git a/src/Preview.vue b/src/Preview.vue
index b799953..270e808 100644
--- a/src/Preview.vue
+++ b/src/Preview.vue
@@ -134,7 +134,7 @@ export default {
// if so, change scheme used by editor
// NOTE: vsg is a superset of JavaScript allowing
// the template to succeed litterally code, very useful for examples
- // vsg stands for vue-styleguidist
+ // NOTE2: vsg stands for vue-styleguidist
this.$emit("detect-language", isCodeVueSfc(code) ? "vue" : "vsg");
// compile and execute the script
@@ -192,9 +192,10 @@ export default {
} else {
this.handleError({
message:
- "[Vue Live] no template or render function specified, you might have an issue in your example",
+ "[Vue Live] no template or render function specified. Example cannot be rendered.",
});
}
+ this.$emit("success");
},
},
};
diff --git a/src/VueLive.vue b/src/VueLive.vue
index f9009c2..7df628e 100644
--- a/src/VueLive.vue
+++ b/src/VueLive.vue
@@ -15,7 +15,9 @@
:delay="delay"
:prism-lang="prismLang"
:editor-props="editorProps"
+ :error="error"
:jsx="jsx"
+ :squiggles="squiggles"
@change="updatePreview"
/>
@@ -30,7 +32,8 @@
:key="codeKey"
:code="model"
@detect-language="switchLanguage"
- @error="(e) => $emit('error', e)"
+ @error="handleError"
+ @success="error = undefined"
:components="components"
:requires="requires"
:jsx="jsx"
@@ -55,7 +58,7 @@ const LANG_TO_PRISM = {
const UPDATE_DELAY = 300;
export default {
- name: "VueLivePreview",
+ name: "VueLive",
components: { Preview, Editor },
props: {
/**
@@ -139,6 +142,14 @@ export default {
type: Boolean,
default: true,
},
+ /**
+ * Show the red markings
+ * where the compiler found errors
+ */
+ squiggles: {
+ type: Boolean,
+ default: true,
+ },
},
data() {
return {
@@ -152,6 +163,7 @@ export default {
* editor repainted every keystroke
*/
stableCode: this.code,
+ error: undefined,
};
},
computed: {
@@ -169,7 +181,6 @@ export default {
updatePreview(code) {
this.stableCode = code;
this.model = code;
-
this.$emit("change", code);
},
switchLanguage(newLang) {
@@ -180,6 +191,10 @@ export default {
this.stableCode = this.model;
}
},
+ handleError(e) {
+ this.error = e;
+ this.$emit("error", e);
+ },
},
};
diff --git a/src/utils/__tests__/checkTemplate.js b/src/utils/__tests__/checkTemplate.js
index c276534..9041266 100644
--- a/src/utils/__tests__/checkTemplate.js
+++ b/src/utils/__tests__/checkTemplate.js
@@ -109,7 +109,7 @@ test("parse invalid template with an error if the value is not in data", () => {
);
});
-test("parse template interpolatio and detect undefined variables", () => {
+test("parse template interpolation and detect lonely undefined variables", () => {
expect(() =>
checkTemplate({
template: "
{{ hello }}
",
@@ -119,6 +119,27 @@ test("parse template interpolatio and detect undefined variables", () => {
);
});
+test("parse template interpolation and detect impacted undefined variables", () => {
+ expect(() =>
+ checkTemplate({
+ template: "{{ hello + 'bonjour' }}
",
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"Variable \\"hello\\" is not defined."`
+ );
+});
+
+test("parse template interpolation and detect impacted right variables", () => {
+ expect(() =>
+ checkTemplate({
+ template:
+ "{{ 'bonjour' + hello + 'sayonara' }}
",
+ })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"Variable \\"hello\\" is not defined."`
+ );
+});
+
test("parse invalid : template by throwing an error", () => {
expect(() =>
checkTemplate({
diff --git a/src/utils/checkTemplate.js b/src/utils/checkTemplate.js
index 41470a4..d8014bc 100644
--- a/src/utils/checkTemplate.js
+++ b/src/utils/checkTemplate.js
@@ -1,4 +1,5 @@
import { parse as parseVue } from "@vue/compiler-dom";
+// force proper english errors
import { createCompilerError } from "@vue/compiler-core/dist/compiler-core.cjs";
import { parse as parseEs } from "acorn";
import { visit } from "recast";
@@ -15,7 +16,7 @@ export default function($options, checkVariableAvailability) {
try {
ast = parseVue($options.template);
} catch (e) {
- throw createCompilerError(e.code);
+ throw createCompilerError(e.code, e.loc);
}
if (!checkVariableAvailability) {
@@ -48,6 +49,12 @@ export default function($options, checkVariableAvailability) {
const templateVars = [];
if (templateAst.type === ELEMENT) {
templateAst.props.forEach((attr) => {
+ if (!/^[a-z,-,:]+$/g.test(attr.name)) {
+ throw new VueLiveParseTemplateAttrError(
+ "[VueLive] Invalid attribute name: " + attr.name,
+ attr.loc
+ );
+ }
const exp =
attr.type !== SIMPLE_EXPRESSION && attr.exp
? attr.exp.content
@@ -95,7 +102,7 @@ export default function($options, checkVariableAvailability) {
...templateVars,
]);
} catch (e) {
- throw new VueLiveParseTemplateError(e.message, exp, e);
+ throw new VueLiveParseTemplateError(e.message, exp, e, attr.loc);
}
}
});
@@ -112,7 +119,8 @@ export default function($options, checkVariableAvailability) {
throw new VueLiveParseTemplateError(
e.message,
templateAst.content,
- e
+ e,
+ templateAst.loc
);
}
}
@@ -134,6 +142,8 @@ export function checkExpression(expression, availableVars, templateVars) {
if (
identifier.name === "expression" ||
identifier.name === "argument" ||
+ identifier.name === "left" ||
+ identifier.name === "right" ||
identifier.parentPath.name === "arguments"
) {
if (
@@ -183,8 +193,14 @@ export function VueLiveUndefinedVariableError(message, varName) {
this.varName = varName;
}
-export function VueLiveParseTemplateError(message, expression, subError) {
+export function VueLiveParseTemplateAttrError(message, loc) {
+ this.message = message;
+ this.loc = loc;
+}
+
+export function VueLiveParseTemplateError(message, expression, subError, loc) {
this.message = message;
this.expression = expression;
this.subError = subError;
+ this.loc = loc;
}
diff --git a/src/utils/highlight.js b/src/utils/highlight.js
new file mode 100644
index 0000000..35bf3fc
--- /dev/null
+++ b/src/utils/highlight.js
@@ -0,0 +1,73 @@
+import {
+ highlight as prismHighlight,
+ languages,
+} from "prismjs/components/prism-core";
+
+import "prismjs/components/prism-clike";
+import "prismjs/components/prism-markup";
+import "prismjs/components/prism-javascript";
+import "prismjs/components/prism-jsx";
+import getScript from "./getScript";
+
+export default (lang, jsxInExamples) => {
+ if (lang === "vsg") {
+ return (code, errorLoc) => {
+ if (!code) {
+ return "";
+ }
+ const scriptCode = getScript(code, jsxInExamples);
+ const scriptCodeHighlighted = prismHighlight(
+ scriptCode,
+ languages[jsxInExamples ? "jsx" : "js"],
+ lang
+ );
+ if (code.length === scriptCode.length) {
+ return getSquiggles(errorLoc) + scriptCodeHighlighted;
+ }
+ const templateCode = code.slice(scriptCode.length);
+ const templateHighlighted = prismHighlight(
+ templateCode,
+ languages["html"],
+ lang
+ );
+
+ return (
+ getSquiggles(
+ errorLoc,
+ errorLoc && errorLoc.start ? scriptCode.split("\n").length - 1 : 0
+ ) +
+ scriptCodeHighlighted +
+ templateHighlighted
+ );
+ };
+ } else {
+ return (code, errorLoc) => {
+ const langScheme = languages[lang];
+ if (!langScheme) {
+ return code;
+ }
+
+ return (
+ // if the error is in the template no need for column padding
+ getSquiggles(errorLoc) + prismHighlight(code, langScheme, lang)
+ );
+ };
+ }
+};
+
+function getSquiggles(errorLoc, lineOffset = 0, columnOffSet = 0) {
+ if (!errorLoc) return "";
+ columnOffSet = errorLoc.start ? 0 : 1;
+ const errorWidth = errorLoc.end
+ ? errorLoc.end.column - errorLoc.start.column + 1
+ : 2;
+ let { line, column } = errorLoc.start ? errorLoc.start : errorLoc;
+ return (
+ '' +
+ Array(line + lineOffset).join("\n") +
+ Array(column + columnOffSet).join(" ") +
+ '' +
+ Array(errorWidth).join(" ") +
+ ""
+ );
+}