From 97cccc956e6ce0539f90372ab81c88046f456a50 Mon Sep 17 00:00:00 2001 From: ElevateBart Date: Thu, 22 Oct 2020 00:12:49 -0500 Subject: [PATCH 1/8] feat: display squiggles on error --- src/Editor.vue | 66 +++++++++++++------------------------- src/Preview.vue | 5 +-- src/VueLive.vue | 12 +++++-- src/utils/checkTemplate.js | 14 +++++--- src/utils/highlight.js | 66 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 109 insertions(+), 54 deletions(-) create mode 100644 src/utils/highlight.js diff --git a/src/Editor.vue b/src/Editor.vue index e31e593..7d52ce7 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..f5ac6ce 100644 --- a/src/Preview.vue +++ b/src/Preview.vue @@ -166,7 +166,7 @@ export default { } try { - checkTemplate(options, this.checkVariableAvailability); + checkTemplate(options, this.checkVariableAvailability, code); } catch (e) { this.handleError(e); return; @@ -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..cffe010 100644 --- a/src/VueLive.vue +++ b/src/VueLive.vue @@ -15,6 +15,7 @@ :delay="delay" :prism-lang="prismLang" :editor-props="editorProps" + :error="error" :jsx="jsx" @change="updatePreview" /> @@ -30,7 +31,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 +57,7 @@ const LANG_TO_PRISM = { const UPDATE_DELAY = 300; export default { - name: "VueLivePreview", + name: "VueLive", components: { Preview, Editor }, props: { /** @@ -152,6 +154,7 @@ export default { * editor repainted every keystroke */ stableCode: this.code, + error: undefined, }; }, computed: { @@ -169,7 +172,6 @@ export default { updatePreview(code) { this.stableCode = code; this.model = code; - this.$emit("change", code); }, switchLanguage(newLang) { @@ -180,6 +182,10 @@ export default { this.stableCode = this.model; } }, + handleError(e) { + this.error = e; + this.$emit("error", e); + }, }, }; diff --git a/src/utils/checkTemplate.js b/src/utils/checkTemplate.js index 41470a4..51a4beb 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"; @@ -7,7 +8,7 @@ const ELEMENT = 1; const SIMPLE_EXPRESSION = 4; const INTERPOLATION = 5; -export default function($options, checkVariableAvailability) { +export default function($options, checkVariableAvailability, code) { if (!$options.template) { return; } @@ -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) { @@ -95,7 +96,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 +113,9 @@ export default function($options, checkVariableAvailability) { throw new VueLiveParseTemplateError( e.message, templateAst.content, - e + e, + templateAst.loc, + code ); } } @@ -183,8 +186,9 @@ export function VueLiveUndefinedVariableError(message, varName) { this.varName = varName; } -export function VueLiveParseTemplateError(message, expression, subError) { +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..33d71a6 --- /dev/null +++ b/src/utils/highlight.js @@ -0,0 +1,66 @@ +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 scriptCodeHighlighted; + } + const templateCode = code.slice(scriptCode.length); + const templateHighlighted = prismHighlight( + templateCode, + languages["html"], + lang + ); + + return ( + getSquiggles(errorLoc, scriptCode.split("\n").length) + + scriptCodeHighlighted + + templateHighlighted + ); + }; + } else { + return (code, errorLoc) => { + const langScheme = languages[lang]; + if (!langScheme) { + return code; + } + + return getSquiggles(errorLoc) + prismHighlight(code, langScheme, lang); + }; + } +}; + +function getSquiggles(errorLoc, lineOffset = 0) { + const errorWidth = + errorLoc && errorLoc.end + ? errorLoc.end.column - errorLoc.start.column + 1 + : 2; + let { line, column } = errorLoc ? errorLoc.start || errorLoc : {}; + return errorLoc + ? ' ' + + Array(errorLoc.start ? line + lineOffset - 1 : line).join("\n") + + Array(column).join(" ") + + '' + + Array(errorWidth).join(" ") + + "" + : ""; +} From fcc1255e2374f45d45fcedba29c5e9340bd2db16 Mon Sep 17 00:00:00 2001 From: ElevateBart Date: Thu, 22 Oct 2020 00:20:58 -0500 Subject: [PATCH 2/8] docs: fix comment --- src/Preview.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Preview.vue b/src/Preview.vue index f5ac6ce..1b0e256 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 From 2e91e4b5506405a4c7154f153957929c7f346cab Mon Sep 17 00:00:00 2001 From: ElevateBart Date: Thu, 22 Oct 2020 01:04:50 -0500 Subject: [PATCH 3/8] fix: invalid attributenames blocking --- src/Preview.vue | 2 +- src/utils/checkTemplate.js | 16 ++++++++--- src/utils/highlight.js | 55 +++++++++++++++++++++++++++----------- 3 files changed, 53 insertions(+), 20 deletions(-) diff --git a/src/Preview.vue b/src/Preview.vue index 1b0e256..270e808 100644 --- a/src/Preview.vue +++ b/src/Preview.vue @@ -166,7 +166,7 @@ export default { } try { - checkTemplate(options, this.checkVariableAvailability, code); + checkTemplate(options, this.checkVariableAvailability); } catch (e) { this.handleError(e); return; diff --git a/src/utils/checkTemplate.js b/src/utils/checkTemplate.js index 51a4beb..63d2aaf 100644 --- a/src/utils/checkTemplate.js +++ b/src/utils/checkTemplate.js @@ -8,7 +8,7 @@ const ELEMENT = 1; const SIMPLE_EXPRESSION = 4; const INTERPOLATION = 5; -export default function($options, checkVariableAvailability, code) { +export default function($options, checkVariableAvailability) { if (!$options.template) { return; } @@ -49,6 +49,12 @@ export default function($options, checkVariableAvailability, code) { 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 @@ -114,8 +120,7 @@ export default function($options, checkVariableAvailability, code) { e.message, templateAst.content, e, - templateAst.loc, - code + templateAst.loc ); } } @@ -186,6 +191,11 @@ export function VueLiveUndefinedVariableError(message, varName) { this.varName = varName; } +export function VueLiveParseTemplateAttrError(message, loc) { + this.message = message; + this.loc = loc; +} + export function VueLiveParseTemplateError(message, expression, subError, loc) { this.message = message; this.expression = expression; diff --git a/src/utils/highlight.js b/src/utils/highlight.js index 33d71a6..fba9a98 100644 --- a/src/utils/highlight.js +++ b/src/utils/highlight.js @@ -32,7 +32,10 @@ export default (lang, jsxInExamples) => { ); return ( - getSquiggles(errorLoc, scriptCode.split("\n").length) + + getSquiggles( + errorLoc, + errorLoc && errorLoc.start ? scriptCode.split("\n").length : 0 + ) + scriptCodeHighlighted + templateHighlighted ); @@ -44,23 +47,43 @@ export default (lang, jsxInExamples) => { return code; } - return getSquiggles(errorLoc) + prismHighlight(code, langScheme, lang); + let lineOffset = 0; + // if we look at a SFC code + if (lang === "html" && errorLoc && errorLoc.start) { + // offset the line to the block where the error has been discovered + // in the template if errorLoc has a start & end + const codeBeforeTemplate = code.split(/