compile.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743
  1. var _ = require('../util')
  2. var publicDirectives = require('../directives/public')
  3. var internalDirectives = require('../directives/internal')
  4. var compileProps = require('./compile-props')
  5. var textParser = require('../parsers/text')
  6. var dirParser = require('../parsers/directive')
  7. var templateParser = require('../parsers/template')
  8. var resolveAsset = _.resolveAsset
  9. // special binding prefixes
  10. var bindRE = /^v-bind:|^:/
  11. var onRE = /^v-on:|^@/
  12. var argRE = /:(.*)$/
  13. var modifierRE = /\.[^\.]+/g
  14. var transitionRE = /^(v-bind:|:)?transition$/
  15. // terminal directives
  16. var terminalDirectives = [
  17. 'for',
  18. 'if'
  19. ]
  20. // default directive priority
  21. var DEFAULT_PRIORITY = 1000
  22. /**
  23. * Compile a template and return a reusable composite link
  24. * function, which recursively contains more link functions
  25. * inside. This top level compile function would normally
  26. * be called on instance root nodes, but can also be used
  27. * for partial compilation if the partial argument is true.
  28. *
  29. * The returned composite link function, when called, will
  30. * return an unlink function that tearsdown all directives
  31. * created during the linking phase.
  32. *
  33. * @param {Element|DocumentFragment} el
  34. * @param {Object} options
  35. * @param {Boolean} partial
  36. * @return {Function}
  37. */
  38. exports.compile = function (el, options, partial) {
  39. // link function for the node itself.
  40. var nodeLinkFn = partial || !options._asComponent
  41. ? compileNode(el, options)
  42. : null
  43. // link function for the childNodes
  44. var childLinkFn =
  45. !(nodeLinkFn && nodeLinkFn.terminal) &&
  46. el.tagName !== 'SCRIPT' &&
  47. el.hasChildNodes()
  48. ? compileNodeList(el.childNodes, options)
  49. : null
  50. /**
  51. * A composite linker function to be called on a already
  52. * compiled piece of DOM, which instantiates all directive
  53. * instances.
  54. *
  55. * @param {Vue} vm
  56. * @param {Element|DocumentFragment} el
  57. * @param {Vue} [host] - host vm of transcluded content
  58. * @param {Object} [scope] - v-for scope
  59. * @param {Fragment} [frag] - link context fragment
  60. * @return {Function|undefined}
  61. */
  62. return function compositeLinkFn (vm, el, host, scope, frag) {
  63. // cache childNodes before linking parent, fix #657
  64. var childNodes = _.toArray(el.childNodes)
  65. // link
  66. var dirs = linkAndCapture(function compositeLinkCapturer () {
  67. if (nodeLinkFn) nodeLinkFn(vm, el, host, scope, frag)
  68. if (childLinkFn) childLinkFn(vm, childNodes, host, scope, frag)
  69. }, vm)
  70. return makeUnlinkFn(vm, dirs)
  71. }
  72. }
  73. /**
  74. * Apply a linker to a vm/element pair and capture the
  75. * directives created during the process.
  76. *
  77. * @param {Function} linker
  78. * @param {Vue} vm
  79. */
  80. function linkAndCapture (linker, vm) {
  81. var originalDirCount = vm._directives.length
  82. linker()
  83. var dirs = vm._directives.slice(originalDirCount)
  84. dirs.sort(directiveComparator)
  85. for (var i = 0, l = dirs.length; i < l; i++) {
  86. dirs[i]._bind()
  87. }
  88. return dirs
  89. }
  90. /**
  91. * Directive priority sort comparator
  92. *
  93. * @param {Object} a
  94. * @param {Object} b
  95. */
  96. function directiveComparator (a, b) {
  97. a = a.descriptor.def.priority || DEFAULT_PRIORITY
  98. b = b.descriptor.def.priority || DEFAULT_PRIORITY
  99. return a > b ? -1 : a === b ? 0 : 1
  100. }
  101. /**
  102. * Linker functions return an unlink function that
  103. * tearsdown all directives instances generated during
  104. * the process.
  105. *
  106. * We create unlink functions with only the necessary
  107. * information to avoid retaining additional closures.
  108. *
  109. * @param {Vue} vm
  110. * @param {Array} dirs
  111. * @param {Vue} [context]
  112. * @param {Array} [contextDirs]
  113. * @return {Function}
  114. */
  115. function makeUnlinkFn (vm, dirs, context, contextDirs) {
  116. return function unlink (destroying) {
  117. teardownDirs(vm, dirs, destroying)
  118. if (context && contextDirs) {
  119. teardownDirs(context, contextDirs)
  120. }
  121. }
  122. }
  123. /**
  124. * Teardown partial linked directives.
  125. *
  126. * @param {Vue} vm
  127. * @param {Array} dirs
  128. * @param {Boolean} destroying
  129. */
  130. function teardownDirs (vm, dirs, destroying) {
  131. var i = dirs.length
  132. while (i--) {
  133. dirs[i]._teardown()
  134. if (!destroying) {
  135. vm._directives.$remove(dirs[i])
  136. }
  137. }
  138. }
  139. /**
  140. * Compile link props on an instance.
  141. *
  142. * @param {Vue} vm
  143. * @param {Element} el
  144. * @param {Object} props
  145. * @param {Object} [scope]
  146. * @return {Function}
  147. */
  148. exports.compileAndLinkProps = function (vm, el, props, scope) {
  149. var propsLinkFn = compileProps(el, props)
  150. var propDirs = linkAndCapture(function () {
  151. propsLinkFn(vm, scope)
  152. }, vm)
  153. return makeUnlinkFn(vm, propDirs)
  154. }
  155. /**
  156. * Compile the root element of an instance.
  157. *
  158. * 1. attrs on context container (context scope)
  159. * 2. attrs on the component template root node, if
  160. * replace:true (child scope)
  161. *
  162. * If this is a fragment instance, we only need to compile 1.
  163. *
  164. * @param {Vue} vm
  165. * @param {Element} el
  166. * @param {Object} options
  167. * @param {Object} contextOptions
  168. * @return {Function}
  169. */
  170. exports.compileRoot = function (el, options, contextOptions) {
  171. var containerAttrs = options._containerAttrs
  172. var replacerAttrs = options._replacerAttrs
  173. var contextLinkFn, replacerLinkFn
  174. // only need to compile other attributes for
  175. // non-fragment instances
  176. if (el.nodeType !== 11) {
  177. // for components, container and replacer need to be
  178. // compiled separately and linked in different scopes.
  179. if (options._asComponent) {
  180. // 2. container attributes
  181. if (containerAttrs && contextOptions) {
  182. contextLinkFn = compileDirectives(containerAttrs, contextOptions)
  183. }
  184. if (replacerAttrs) {
  185. // 3. replacer attributes
  186. replacerLinkFn = compileDirectives(replacerAttrs, options)
  187. }
  188. } else {
  189. // non-component, just compile as a normal element.
  190. replacerLinkFn = compileDirectives(el.attributes, options)
  191. }
  192. } else if (process.env.NODE_ENV !== 'production' && containerAttrs) {
  193. // warn container directives for fragment instances
  194. var names = containerAttrs
  195. .filter(function (attr) {
  196. // allow vue-loader/vueify scoped css attributes
  197. return attr.name.indexOf('_v-') < 0 &&
  198. // allow event listeners
  199. !onRE.test(attr.name) &&
  200. // allow slots
  201. attr.name !== 'slot'
  202. })
  203. .map(function (attr) {
  204. return '"' + attr.name + '"'
  205. })
  206. if (names.length) {
  207. var plural = names.length > 1
  208. _.warn(
  209. 'Attribute' + (plural ? 's ' : ' ') + names.join(', ') +
  210. (plural ? ' are' : ' is') + ' ignored on component ' +
  211. '<' + options.el.tagName.toLowerCase() + '> because ' +
  212. 'the component is a fragment instance: ' +
  213. 'http://vuejs.org/guide/components.html#Fragment_Instance'
  214. )
  215. }
  216. }
  217. return function rootLinkFn (vm, el, scope) {
  218. // link context scope dirs
  219. var context = vm._context
  220. var contextDirs
  221. if (context && contextLinkFn) {
  222. contextDirs = linkAndCapture(function () {
  223. contextLinkFn(context, el, null, scope)
  224. }, context)
  225. }
  226. // link self
  227. var selfDirs = linkAndCapture(function () {
  228. if (replacerLinkFn) replacerLinkFn(vm, el)
  229. }, vm)
  230. // return the unlink function that tearsdown context
  231. // container directives.
  232. return makeUnlinkFn(vm, selfDirs, context, contextDirs)
  233. }
  234. }
  235. /**
  236. * Compile a node and return a nodeLinkFn based on the
  237. * node type.
  238. *
  239. * @param {Node} node
  240. * @param {Object} options
  241. * @return {Function|null}
  242. */
  243. function compileNode (node, options) {
  244. var type = node.nodeType
  245. if (type === 1 && node.tagName !== 'SCRIPT') {
  246. return compileElement(node, options)
  247. } else if (type === 3 && node.data.trim()) {
  248. return compileTextNode(node, options)
  249. } else {
  250. return null
  251. }
  252. }
  253. /**
  254. * Compile an element and return a nodeLinkFn.
  255. *
  256. * @param {Element} el
  257. * @param {Object} options
  258. * @return {Function|null}
  259. */
  260. function compileElement (el, options) {
  261. // preprocess textareas.
  262. // textarea treats its text content as the initial value.
  263. // just bind it as an attr directive for value.
  264. if (el.tagName === 'TEXTAREA') {
  265. var tokens = textParser.parse(el.value)
  266. if (tokens) {
  267. el.setAttribute(':value', textParser.tokensToExp(tokens))
  268. el.value = ''
  269. }
  270. }
  271. var linkFn
  272. var hasAttrs = el.hasAttributes()
  273. // check terminal directives (for & if)
  274. if (hasAttrs) {
  275. linkFn = checkTerminalDirectives(el, options)
  276. }
  277. // check element directives
  278. if (!linkFn) {
  279. linkFn = checkElementDirectives(el, options)
  280. }
  281. // check component
  282. if (!linkFn) {
  283. linkFn = checkComponent(el, options)
  284. }
  285. // normal directives
  286. if (!linkFn && hasAttrs) {
  287. linkFn = compileDirectives(el.attributes, options)
  288. }
  289. return linkFn
  290. }
  291. /**
  292. * Compile a textNode and return a nodeLinkFn.
  293. *
  294. * @param {TextNode} node
  295. * @param {Object} options
  296. * @return {Function|null} textNodeLinkFn
  297. */
  298. function compileTextNode (node, options) {
  299. var tokens = textParser.parse(node.data)
  300. if (!tokens) {
  301. return null
  302. }
  303. var frag = document.createDocumentFragment()
  304. var el, token
  305. for (var i = 0, l = tokens.length; i < l; i++) {
  306. token = tokens[i]
  307. el = token.tag
  308. ? processTextToken(token, options)
  309. : document.createTextNode(token.value)
  310. frag.appendChild(el)
  311. }
  312. return makeTextNodeLinkFn(tokens, frag, options)
  313. }
  314. /**
  315. * Process a single text token.
  316. *
  317. * @param {Object} token
  318. * @param {Object} options
  319. * @return {Node}
  320. */
  321. function processTextToken (token, options) {
  322. var el
  323. if (token.oneTime) {
  324. el = document.createTextNode(token.value)
  325. } else {
  326. if (token.html) {
  327. el = document.createComment('v-html')
  328. setTokenType('html')
  329. } else {
  330. // IE will clean up empty textNodes during
  331. // frag.cloneNode(true), so we have to give it
  332. // something here...
  333. el = document.createTextNode(' ')
  334. setTokenType('text')
  335. }
  336. }
  337. function setTokenType (type) {
  338. if (token.descriptor) return
  339. var parsed = dirParser.parse(token.value)
  340. token.descriptor = {
  341. name: type,
  342. def: publicDirectives[type],
  343. expression: parsed.expression,
  344. filters: parsed.filters
  345. }
  346. }
  347. return el
  348. }
  349. /**
  350. * Build a function that processes a textNode.
  351. *
  352. * @param {Array<Object>} tokens
  353. * @param {DocumentFragment} frag
  354. */
  355. function makeTextNodeLinkFn (tokens, frag) {
  356. return function textNodeLinkFn (vm, el, host, scope) {
  357. var fragClone = frag.cloneNode(true)
  358. var childNodes = _.toArray(fragClone.childNodes)
  359. var token, value, node
  360. for (var i = 0, l = tokens.length; i < l; i++) {
  361. token = tokens[i]
  362. value = token.value
  363. if (token.tag) {
  364. node = childNodes[i]
  365. if (token.oneTime) {
  366. value = (scope || vm).$eval(value)
  367. if (token.html) {
  368. _.replace(node, templateParser.parse(value, true))
  369. } else {
  370. node.data = value
  371. }
  372. } else {
  373. vm._bindDir(token.descriptor, node, host, scope)
  374. }
  375. }
  376. }
  377. _.replace(el, fragClone)
  378. }
  379. }
  380. /**
  381. * Compile a node list and return a childLinkFn.
  382. *
  383. * @param {NodeList} nodeList
  384. * @param {Object} options
  385. * @return {Function|undefined}
  386. */
  387. function compileNodeList (nodeList, options) {
  388. var linkFns = []
  389. var nodeLinkFn, childLinkFn, node
  390. for (var i = 0, l = nodeList.length; i < l; i++) {
  391. node = nodeList[i]
  392. nodeLinkFn = compileNode(node, options)
  393. childLinkFn =
  394. !(nodeLinkFn && nodeLinkFn.terminal) &&
  395. node.tagName !== 'SCRIPT' &&
  396. node.hasChildNodes()
  397. ? compileNodeList(node.childNodes, options)
  398. : null
  399. linkFns.push(nodeLinkFn, childLinkFn)
  400. }
  401. return linkFns.length
  402. ? makeChildLinkFn(linkFns)
  403. : null
  404. }
  405. /**
  406. * Make a child link function for a node's childNodes.
  407. *
  408. * @param {Array<Function>} linkFns
  409. * @return {Function} childLinkFn
  410. */
  411. function makeChildLinkFn (linkFns) {
  412. return function childLinkFn (vm, nodes, host, scope, frag) {
  413. var node, nodeLinkFn, childrenLinkFn
  414. for (var i = 0, n = 0, l = linkFns.length; i < l; n++) {
  415. node = nodes[n]
  416. nodeLinkFn = linkFns[i++]
  417. childrenLinkFn = linkFns[i++]
  418. // cache childNodes before linking parent, fix #657
  419. var childNodes = _.toArray(node.childNodes)
  420. if (nodeLinkFn) {
  421. nodeLinkFn(vm, node, host, scope, frag)
  422. }
  423. if (childrenLinkFn) {
  424. childrenLinkFn(vm, childNodes, host, scope, frag)
  425. }
  426. }
  427. }
  428. }
  429. /**
  430. * Check for element directives (custom elements that should
  431. * be resovled as terminal directives).
  432. *
  433. * @param {Element} el
  434. * @param {Object} options
  435. */
  436. function checkElementDirectives (el, options) {
  437. var tag = el.tagName.toLowerCase()
  438. if (_.commonTagRE.test(tag)) return
  439. var def = resolveAsset(options, 'elementDirectives', tag)
  440. if (def) {
  441. return makeTerminalNodeLinkFn(el, tag, '', options, def)
  442. }
  443. }
  444. /**
  445. * Check if an element is a component. If yes, return
  446. * a component link function.
  447. *
  448. * @param {Element} el
  449. * @param {Object} options
  450. * @return {Function|undefined}
  451. */
  452. function checkComponent (el, options) {
  453. var component = _.checkComponent(el, options)
  454. if (component) {
  455. var ref = _.findRef(el)
  456. var descriptor = {
  457. name: 'component',
  458. ref: ref,
  459. expression: component.id,
  460. def: internalDirectives.component,
  461. modifiers: {
  462. literal: !component.dynamic
  463. }
  464. }
  465. var componentLinkFn = function (vm, el, host, scope, frag) {
  466. if (ref) {
  467. _.defineReactive((scope || vm).$refs, ref, null)
  468. }
  469. vm._bindDir(descriptor, el, host, scope, frag)
  470. }
  471. componentLinkFn.terminal = true
  472. return componentLinkFn
  473. }
  474. }
  475. /**
  476. * Check an element for terminal directives in fixed order.
  477. * If it finds one, return a terminal link function.
  478. *
  479. * @param {Element} el
  480. * @param {Object} options
  481. * @return {Function} terminalLinkFn
  482. */
  483. function checkTerminalDirectives (el, options) {
  484. // skip v-pre
  485. if (_.attr(el, 'v-pre') !== null) {
  486. return skip
  487. }
  488. // skip v-else block, but only if following v-if
  489. if (el.hasAttribute('v-else')) {
  490. var prev = el.previousElementSibling
  491. if (prev && prev.hasAttribute('v-if')) {
  492. return skip
  493. }
  494. }
  495. var value, dirName
  496. for (var i = 0, l = terminalDirectives.length; i < l; i++) {
  497. dirName = terminalDirectives[i]
  498. /* eslint-disable no-cond-assign */
  499. if (value = el.getAttribute('v-' + dirName)) {
  500. return makeTerminalNodeLinkFn(el, dirName, value, options)
  501. }
  502. /* eslint-enable no-cond-assign */
  503. }
  504. }
  505. function skip () {}
  506. skip.terminal = true
  507. /**
  508. * Build a node link function for a terminal directive.
  509. * A terminal link function terminates the current
  510. * compilation recursion and handles compilation of the
  511. * subtree in the directive.
  512. *
  513. * @param {Element} el
  514. * @param {String} dirName
  515. * @param {String} value
  516. * @param {Object} options
  517. * @param {Object} [def]
  518. * @return {Function} terminalLinkFn
  519. */
  520. function makeTerminalNodeLinkFn (el, dirName, value, options, def) {
  521. var parsed = dirParser.parse(value)
  522. var descriptor = {
  523. name: dirName,
  524. expression: parsed.expression,
  525. filters: parsed.filters,
  526. raw: value,
  527. // either an element directive, or if/for
  528. def: def || publicDirectives[dirName]
  529. }
  530. // check ref for v-for
  531. if (dirName === 'for') {
  532. descriptor.ref = _.findRef(el)
  533. }
  534. var fn = function terminalNodeLinkFn (vm, el, host, scope, frag) {
  535. if (descriptor.ref) {
  536. _.defineReactive((scope || vm).$refs, descriptor.ref, null)
  537. }
  538. vm._bindDir(descriptor, el, host, scope, frag)
  539. }
  540. fn.terminal = true
  541. return fn
  542. }
  543. /**
  544. * Compile the directives on an element and return a linker.
  545. *
  546. * @param {Array|NamedNodeMap} attrs
  547. * @param {Object} options
  548. * @return {Function}
  549. */
  550. function compileDirectives (attrs, options) {
  551. var i = attrs.length
  552. var dirs = []
  553. var attr, name, value, rawName, rawValue, dirName, arg, modifiers, dirDef, tokens
  554. while (i--) {
  555. attr = attrs[i]
  556. name = rawName = attr.name
  557. value = rawValue = attr.value
  558. tokens = textParser.parse(value)
  559. // reset arg
  560. arg = null
  561. // check modifiers
  562. modifiers = parseModifiers(name)
  563. name = name.replace(modifierRE, '')
  564. // attribute interpolations
  565. if (tokens) {
  566. value = textParser.tokensToExp(tokens)
  567. arg = name
  568. pushDir('bind', publicDirectives.bind, true)
  569. // warn against mixing mustaches with v-bind
  570. if (process.env.NODE_ENV !== 'production') {
  571. if (name === 'class' && Array.prototype.some.call(attrs, function (attr) {
  572. return attr.name === ':class' || attr.name === 'v-bind:class'
  573. })) {
  574. _.warn(
  575. 'class="' + rawValue + '": Do not mix mustache interpolation ' +
  576. 'and v-bind for "class" on the same element. Use one or the other.'
  577. )
  578. }
  579. }
  580. } else
  581. // special attribute: transition
  582. if (transitionRE.test(name)) {
  583. modifiers.literal = !bindRE.test(name)
  584. pushDir('transition', internalDirectives.transition)
  585. } else
  586. // event handlers
  587. if (onRE.test(name)) {
  588. arg = name.replace(onRE, '')
  589. pushDir('on', publicDirectives.on)
  590. } else
  591. // attribute bindings
  592. if (bindRE.test(name)) {
  593. dirName = name.replace(bindRE, '')
  594. if (dirName === 'style' || dirName === 'class') {
  595. pushDir(dirName, internalDirectives[dirName])
  596. } else {
  597. arg = dirName
  598. pushDir('bind', publicDirectives.bind)
  599. }
  600. } else
  601. // normal directives
  602. if (name.indexOf('v-') === 0) {
  603. // check arg
  604. arg = (arg = name.match(argRE)) && arg[1]
  605. if (arg) {
  606. name = name.replace(argRE, '')
  607. }
  608. // extract directive name
  609. dirName = name.slice(2)
  610. // skip v-else (when used with v-show)
  611. if (dirName === 'else') {
  612. continue
  613. }
  614. dirDef = resolveAsset(options, 'directives', dirName)
  615. if (process.env.NODE_ENV !== 'production') {
  616. _.assertAsset(dirDef, 'directive', dirName)
  617. }
  618. if (dirDef) {
  619. pushDir(dirName, dirDef)
  620. }
  621. }
  622. }
  623. /**
  624. * Push a directive.
  625. *
  626. * @param {String} dirName
  627. * @param {Object|Function} def
  628. * @param {Boolean} [interp]
  629. */
  630. function pushDir (dirName, def, interp) {
  631. var parsed = dirParser.parse(value)
  632. dirs.push({
  633. name: dirName,
  634. attr: rawName,
  635. raw: rawValue,
  636. def: def,
  637. arg: arg,
  638. modifiers: modifiers,
  639. expression: parsed.expression,
  640. filters: parsed.filters,
  641. interp: interp
  642. })
  643. }
  644. if (dirs.length) {
  645. return makeNodeLinkFn(dirs)
  646. }
  647. }
  648. /**
  649. * Parse modifiers from directive attribute name.
  650. *
  651. * @param {String} name
  652. * @return {Object}
  653. */
  654. function parseModifiers (name) {
  655. var res = Object.create(null)
  656. var match = name.match(modifierRE)
  657. if (match) {
  658. var i = match.length
  659. while (i--) {
  660. res[match[i].slice(1)] = true
  661. }
  662. }
  663. return res
  664. }
  665. /**
  666. * Build a link function for all directives on a single node.
  667. *
  668. * @param {Array} directives
  669. * @return {Function} directivesLinkFn
  670. */
  671. function makeNodeLinkFn (directives) {
  672. return function nodeLinkFn (vm, el, host, scope, frag) {
  673. // reverse apply because it's sorted low to high
  674. var i = directives.length
  675. while (i--) {
  676. vm._bindDir(directives[i], el, host, scope, frag)
  677. }
  678. }
  679. }