compile.js 17 KB

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