You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
228 lines
6.5 KiB
228 lines
6.5 KiB
/** |
|
* @author Yosuke Ota |
|
* See LICENSE file in root directory for full license. |
|
*/ |
|
'use strict' |
|
|
|
const utils = require('../../../utils') |
|
/** |
|
* @typedef {object} SlotVForVariables |
|
* @property {VForExpression} expr |
|
* @property {VVariable[]} variables |
|
*/ |
|
/** |
|
* @typedef {object} SlotContext |
|
* @property {VElement} element |
|
* @property {VAttribute | VDirective | null} slot |
|
* @property {VDirective | null} vFor |
|
* @property {SlotVForVariables | null} slotVForVars |
|
* @property {string} normalizedName |
|
*/ |
|
/** |
|
* Checks whether the given element can use v-slot. |
|
* @param {VElement} element |
|
* @param {SourceCode} sourceCode |
|
* @param {ParserServices.TokenStore} tokenStore |
|
*/ |
|
module.exports = function canConvertToVSlot(element, sourceCode, tokenStore) { |
|
if (element.name !== 'template') { |
|
return false |
|
} |
|
const ownerElement = element.parent |
|
if ( |
|
ownerElement.type === 'VDocumentFragment' || |
|
!utils.isCustomComponent(ownerElement) |
|
) { |
|
return false |
|
} |
|
const slot = getSlotContext(element, sourceCode) |
|
if (slot.vFor && !slot.slotVForVars) { |
|
// E.g., <template v-for="x of xs" #one></template> |
|
return false |
|
} |
|
if (hasSameSlotDirective(ownerElement, slot, sourceCode, tokenStore)) { |
|
return false |
|
} |
|
return true |
|
} |
|
/** |
|
* @param {VElement} element |
|
* @param {SourceCode} sourceCode |
|
* @returns {SlotContext} |
|
*/ |
|
function getSlotContext(element, sourceCode) { |
|
const slot = |
|
utils.getAttribute(element, 'slot') || |
|
utils.getDirective(element, 'bind', 'slot') |
|
const vFor = utils.getDirective(element, 'for') |
|
const slotVForVars = getSlotVForVariableIfUsingIterationVars(slot, vFor) |
|
|
|
return { |
|
element, |
|
slot, |
|
vFor, |
|
slotVForVars, |
|
normalizedName: getNormalizedName(slot, sourceCode) |
|
} |
|
} |
|
|
|
/** |
|
* Gets the `v-for` directive and variable that provide the variables used by the given `slot` attribute. |
|
* @param {VAttribute | VDirective | null} slot The current `slot` attribute node. |
|
* @param {VDirective | null} [vFor] The current `v-for` directive node. |
|
* @returns { SlotVForVariables | null } The SlotVForVariables. |
|
*/ |
|
function getSlotVForVariableIfUsingIterationVars(slot, vFor) { |
|
if (!slot || !slot.directive) { |
|
return null |
|
} |
|
const expr = |
|
vFor && vFor.value && /** @type {VForExpression} */ (vFor.value.expression) |
|
const variables = |
|
expr && getUsingIterationVars(slot.value, slot.parent.parent) |
|
return expr && variables && variables.length ? { expr, variables } : null |
|
} |
|
|
|
/** |
|
* Gets iterative variables if a given expression node is using iterative variables that the element defined. |
|
* @param {VExpressionContainer|null} expression The expression node to check. |
|
* @param {VElement} element The element node which has the expression. |
|
* @returns {VVariable[]} The expression node is using iteration variables. |
|
*/ |
|
function getUsingIterationVars(expression, element) { |
|
const vars = [] |
|
if (expression && expression.type === 'VExpressionContainer') { |
|
for (const { variable } of expression.references) { |
|
if ( |
|
variable != null && |
|
variable.kind === 'v-for' && |
|
variable.id.range[0] > element.startTag.range[0] && |
|
variable.id.range[1] < element.startTag.range[1] |
|
) { |
|
vars.push(variable) |
|
} |
|
} |
|
} |
|
return vars |
|
} |
|
|
|
/** |
|
* Get the normalized name of a given `slot` attribute node. |
|
* @param {VAttribute | VDirective | null} slotAttr node of `slot` |
|
* @param {SourceCode} sourceCode The source code. |
|
* @returns {string} The normalized name. |
|
*/ |
|
function getNormalizedName(slotAttr, sourceCode) { |
|
if (!slotAttr) { |
|
return 'default' |
|
} |
|
if (!slotAttr.directive) { |
|
return slotAttr.value ? slotAttr.value.value : 'default' |
|
} |
|
return slotAttr.value ? `[${sourceCode.getText(slotAttr.value)}]` : '[null]' |
|
} |
|
|
|
/** |
|
* Checks whether parent element has the same slot as the given slot. |
|
* @param {VElement} ownerElement The parent element. |
|
* @param {SlotContext} targetSlot The SlotContext with a slot to check if they are the same. |
|
* @param {SourceCode} sourceCode |
|
* @param {ParserServices.TokenStore} tokenStore |
|
*/ |
|
function hasSameSlotDirective( |
|
ownerElement, |
|
targetSlot, |
|
sourceCode, |
|
tokenStore |
|
) { |
|
for (const group of utils.iterateChildElementsChains(ownerElement)) { |
|
if (group.includes(targetSlot.element)) { |
|
continue |
|
} |
|
for (const childElement of group) { |
|
const slot = getSlotContext(childElement, sourceCode) |
|
if (!targetSlot.slotVForVars || !slot.slotVForVars) { |
|
if ( |
|
!targetSlot.slotVForVars && |
|
!slot.slotVForVars && |
|
targetSlot.normalizedName === slot.normalizedName |
|
) { |
|
return true |
|
} |
|
continue |
|
} |
|
if ( |
|
equalSlotVForVariables( |
|
targetSlot.slotVForVars, |
|
slot.slotVForVars, |
|
tokenStore |
|
) |
|
) { |
|
return true |
|
} |
|
} |
|
} |
|
return false |
|
} |
|
|
|
/** |
|
* Determines whether the two given `v-slot` variables are considered to be equal. |
|
* @param {SlotVForVariables} a First element. |
|
* @param {SlotVForVariables} b Second element. |
|
* @param {ParserServices.TokenStore} tokenStore The token store. |
|
* @returns {boolean} `true` if the elements are considered to be equal. |
|
*/ |
|
function equalSlotVForVariables(a, b, tokenStore) { |
|
if (a.variables.length !== b.variables.length) { |
|
return false |
|
} |
|
if (!equal(a.expr.right, b.expr.right)) { |
|
return false |
|
} |
|
|
|
const checkedVarNames = new Set() |
|
const len = Math.min(a.expr.left.length, b.expr.left.length) |
|
for (let index = 0; index < len; index++) { |
|
const aPtn = a.expr.left[index] |
|
const bPtn = b.expr.left[index] |
|
|
|
const aVar = a.variables.find( |
|
(v) => aPtn.range[0] <= v.id.range[0] && v.id.range[1] <= aPtn.range[1] |
|
) |
|
const bVar = b.variables.find( |
|
(v) => bPtn.range[0] <= v.id.range[0] && v.id.range[1] <= bPtn.range[1] |
|
) |
|
if (aVar && bVar) { |
|
if (aVar.id.name !== bVar.id.name) { |
|
return false |
|
} |
|
if (!equal(aPtn, bPtn)) { |
|
return false |
|
} |
|
checkedVarNames.add(aVar.id.name) |
|
} else if (aVar || bVar) { |
|
return false |
|
} |
|
} |
|
for (const v of a.variables) { |
|
if (!checkedVarNames.has(v.id.name)) { |
|
if (b.variables.every((bv) => v.id.name !== bv.id.name)) { |
|
return false |
|
} |
|
} |
|
} |
|
return true |
|
|
|
/** |
|
* Determines whether the two given nodes are considered to be equal. |
|
* @param {ASTNode} a First node. |
|
* @param {ASTNode} b Second node. |
|
* @returns {boolean} `true` if the nodes are considered to be equal. |
|
*/ |
|
function equal(a, b) { |
|
if (a.type !== b.type) { |
|
return false |
|
} |
|
return utils.equalTokens(a, b, tokenStore) |
|
} |
|
}
|
|
|