UNPKG

html-validate-vue

Version:

vue transform for html-validate

525 lines (502 loc) 13.2 kB
"use strict"; // src/index.ts var import_html_validate6 = require("html-validate"); // package.json var name = "html-validate-vue"; var peerDependencies = { "html-validate": "^8.10.0 || ^9.0.0" }; // elements.json var elements_default = { component: { flow: true, phrasing: true, transparent: true, component: "is", requiredAttributes: ["is"] }, "keep-alive": { flow: true, phrasing: true, transparent: true }, portal: { deprecated: { source: "vue", message: "the <portal> element has been renamed to <teleport>", documentation: "The `<portal>` element has been renamed to `<teleport>`" } }, "router-link": { flow: true, phrasing: true, transparent: true }, "router-view": { flow: true, slots: ["default"] }, slot: { flow: true, phrasing: true, transparent: true, scriptSupporting: true }, suspense: { flow: true, phrasing: true, transparent: true, slots: ["default", "fallback"] }, teleport: { flow: true, phrasing: true }, transition: { flow: true, phrasing: true, transparent: true }, "transition-group": { flow: true, phrasing: true, transparent: true, component: "tag" } }; // src/configs/recommended.ts function createConfig() { const config = { elements: [elements_default], rules: { /* vue modifiers often use camelcase so allow by default */ "attr-case": ["error", { style: "camelcase" }], /* self closing tags often used in vue code so allow by default*/ "void-style": ["error", { style: "selfclose" }], "no-self-closing": "off", /* enable rules from this plugin */ "vue/available-slots": "error", "vue/prefer-slot-shorthand": "error", "vue/required-slots": "error" } }; return config; } var recommended_default = createConfig(); // src/configs/index.ts var configs_default = { recommended: recommended_default }; // src/rules/available-slots.ts var import_html_validate = require("html-validate"); // src/utils/strip-templating.ts function stripTemplating(str) { return str.replace(/{{(.*?)}}/g, (_, m) => { const filler = " ".repeat(m.length); return `{{${filler}}}`; }); } // src/utils/slots.ts var slotAttr = /(?:v-slot:|#)([^[]+)/; function haveSlot(value) { return value !== null; } function findUsedSlots(node) { const template = node.querySelectorAll(">template, >[slot]"); const slots = template.map((child) => findSlotAttribute(child)).filter(haveSlot); const initial = /* @__PURE__ */ new Map(); return slots.reduce((result, [key, val]) => { result.set(key, val); return result; }, initial); } function findSlotAttribute(node) { for (const attr of node.attributes) { const match = slotAttr.exec(attr.key); if (match) { const [, slot] = match; return [slot, attr.keyLocation]; } if (attr.key === "slot" && typeof attr.value === "string") { return [attr.value.toString(), attr.valueLocation]; } } return null; } // src/rules/available-slots.ts function difference(a, b) { const result = new Set(a); for (const elem of b) { result.delete(elem); } return result; } var AvailableSlots = class extends import_html_validate.Rule { documentation(context) { if (context) { return { description: [ `The <${context.element}> component does not have a slot named "${context.slot}". Only known slots can be specified.`, "", "The following slots are available" ].concat(context.available.map((slot) => `- ${slot}`)).join("\n") }; } else { return { description: "Only known slots can be specified." }; } } setup() { this.on("dom:ready", (event) => { const doc = event.document; doc.visitDepthFirst((node) => { const meta = node.meta; if (!meta?.slots) { return; } this.validateSlots(node, meta.slots); }); }); } validateSlots(node, slots) { const usedSlots = findUsedSlots(node); const available = new Set(slots); const used = new Set(usedSlots.keys()); const diff = difference(used, available); for (const missing of diff) { const context = { element: node.tagName, slot: missing, available: slots }; this.report( node, `<${node.tagName}> component has no slot "${missing}"`, usedSlots.get(missing), context ); } } }; // src/rules/prefer-slot-shorthand.ts var import_html_validate2 = require("html-validate"); var prefix = "v-slot:"; function isVSlot(attr) { if (!attr.startsWith(prefix)) { return null; } if (attr.length <= prefix.length) { return null; } const slot = attr.slice(prefix.length); if (/^\[.*\]$/.exec(slot)) { return null; } return slot; } function isDeprecatedSlot(attr, value) { if (attr !== "slot") { return null; } if (value instanceof import_html_validate2.DynamicValue) { return null; } if (value === null || value === "") { return null; } return value.toString(); } var PreferSlotShorthand = class extends import_html_validate2.Rule { documentation(context) { const slot = context?.slot ?? "my-slot"; return { description: [ `Prefer to use \`#${slot}\` over \`v-slot:${slot}\`.`, "", "Vue.js supports a shorthand syntax for slots using `#` instead of `v-slot:` or the deprecated `slot` attribute, e.g. using `#header` instead of `v-slot:header`." ].join("\n") }; } setup() { this.on("attr", (event) => { const { key: attr, value, target, keyLocation: location } = event; let slot; slot = isVSlot(attr); if (slot) { const context = { slot }; const message = `Prefer to use #${slot} over ${attr}`; this.report(target, message, location, context); } slot = isDeprecatedSlot(attr, value); if (slot) { const context = { slot }; const message = `Prefer to use #${slot} over ${attr}="${value.toString()}"`; this.report(target, message, location, context); } }); } }; // src/rules/required-slots.ts var import_html_validate3 = require("html-validate"); function difference2(a, b) { const result = new Set(a); for (const elem of b) { result.delete(elem); } return result; } var RequiredSlots = class extends import_html_validate3.Rule { documentation(context) { if (context) { return { description: [ `The <${context.element}> component requires slot "${context.slot}" to be implemented. Add \`<template v-slot:${context.slot}>\``, "", "The following slots are required" ].concat(context.required.map((slot) => `- ${slot}`)).join("\n") }; } else { return { description: "All required slots must be implemented." }; } } setup() { this.on("dom:ready", (event) => { const doc = event.document; doc.visitDepthFirst((node) => { const meta = node.meta; if (!meta?.requiredSlots) { return; } this.validateSlots(node, meta.requiredSlots); }); }); } validateSlots(node, requiredSlots) { const usedSlots = findUsedSlots(node); const required = new Set(requiredSlots); const used = new Set(usedSlots.keys()); const diff = difference2(required, used); for (const missing of diff) { const context = { element: node.tagName, slot: missing, required: requiredSlots }; this.report( node, `<${node.tagName}> component requires slot "${missing}" to be implemented`, null, context ); } } }; // src/rules/index.ts var rules = { "vue/available-slots": AvailableSlots, "vue/prefer-slot-shorthand": PreferSlotShorthand, "vue/required-slots": RequiredSlots }; var rules_default = rules; // src/hooks/attribute.ts var import_html_validate4 = require("html-validate"); function* processAttribute(attr) { yield attr; const bind = /^(?:v-bind)?:(.*)$/.exec(attr.key); if (bind) { yield { ...attr, key: bind[1], value: new import_html_validate4.DynamicValue(attr.value ? String(attr.value) : ""), originalAttribute: attr.key }; } } // src/hooks/element.ts var import_html_validate5 = require("html-validate"); function isBound(node) { return node.hasAttribute("v-html"); } function isSlot(node) { return node.is("slot"); } function loadSlotMeta(context, slotname, node) { const parent = node.parent; if (!parent) { return; } const getMetaFor = (tagName) => { const meta2 = context.getMetaFor(tagName); return meta2 && meta2.tagName !== "*" ? meta2 : null; }; const key = `${parent.tagName}#${slotname}`; const fallbackKey = `${parent.tagName}:${slotname}`; const meta = getMetaFor(key) ?? getMetaFor(fallbackKey); if (!meta) { return; } node.loadMeta(meta); node.setAnnotation(`slot "${slotname}" (<${parent.tagName}>)`); } function loadComponentMeta(context, node, component) { let src = node; if (node.tagName === "template" && src.parent) { src = src.parent; } const attr = src.getAttribute(component); if (!attr || attr.isDynamic) { return; } const tagName = attr.value; const meta = context.getMetaFor(tagName); if (!meta || meta.tagName === "*") { return; } node.loadMeta(meta); if (node.isSameNode(src)) { node.setAnnotation(`<${meta.tagName}> (<${node.tagName}>)`); } else { const name2 = node.annotatedName.replace("(<", `(<${meta.tagName}> via <`); node.setAnnotation(name2); } } function processElement(node) { const slot = findSlotAttribute(node); if (slot) { const [slotname] = slot; loadSlotMeta(this, slotname, node); } if (node.meta?.component) { loadComponentMeta(this, node, node.meta.component); } if (isBound(node) || isSlot(node)) { node.appendText(new import_html_validate5.DynamicValue(""), node.location); } } // src/transform/html.transform.ts function* transformHTML(source) { yield { ...source, hooks: { processAttribute, processElement } }; } transformHTML.api = 1; // src/transform/js.transform.ts var import_plugin_utils = require("@html-validate/plugin-utils"); function transformJS(source) { const te = import_plugin_utils.TemplateExtractor.fromString(source.data, source.filename); return Array.from(te.extractObjectProperty("template"), (cur) => { cur.originalData = source.originalData ?? source.data; cur.hooks = { processAttribute, processElement }; return cur; }); } transformJS.api = 1; // src/transform/sfc.transform.ts function findLocation(source, index, preamble) { let line = 1; let prev = 0; let pos = source.indexOf("\n"); while (pos !== -1) { if (pos > index) { return [line, index - prev + preamble + 1]; } line++; prev = pos; pos = source.indexOf("\n", pos + 1); } return [line, 1]; } function* transformSFC(source) { const sfc = /^(<template([^>]*)>)([^]*?)^<\/template>/gm; let match; while ((match = sfc.exec(source.data)) !== null) { const [, preamble, attr, data] = match; if (!/lang=".*?"/.exec(attr)) { const [line, column] = findLocation(source.data, match.index, preamble.length); yield { data: stripTemplating(data), filename: source.filename, line: line + (source.line - 1), column: column + (source.column - 1), offset: match.index + (source.offset || 0) + preamble.length, originalData: source.originalData ?? source.data, hooks: { processAttribute, processElement } }; } } } transformSFC.api = 1; // src/transform/auto.transform.ts function isVue(source) { return !!/\.vue$/i.exec(source.filename); } function isJavascript(source) { return !!/\.(jsx?|tsx?)$/i.exec(source.filename); } function* autodetect(source) { if (isVue(source)) { yield* transformSFC(source); } else if (isJavascript(source)) { yield* transformJS(source); } else { yield* transformHTML(source); } } autodetect.api = 1; // src/transform/index.ts var transformer = { default: autodetect, auto: autodetect, js: transformJS, sfc: transformSFC, html: transformHTML }; var transform_default = transformer; // src/schema.json var schema_default = { properties: { component: { type: "string", copyable: true }, requiredSlots: { type: "array", items: { type: "string" } }, slots: { type: "array", items: { type: "string" } } } }; // src/index.ts var range = peerDependencies["html-validate"]; (0, import_html_validate6.compatibilityCheck)(name, range); var plugin = { name, configs: configs_default, rules: rules_default, transformer: transform_default, elementSchema: schema_default }; var src_default = plugin; // src/entry-cjs.ts module.exports = src_default; //# sourceMappingURL=index.cjs.map