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 @@ @@ -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(" ") + + "" + ); +}