compile.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744
  1. var _ = require('../util')
  2. var config = require('../config')
  3. var textParser = require('../parsers/text')
  4. var dirParser = require('../parsers/directive')
  5. var templateParser = require('../parsers/template')
  6. var resolveAsset = _.resolveAsset
  7. // internal directives
  8. var propDef = require('../directives/prop')
  9. var componentDef = require('../directives/component')
  10. // terminal directives
  11. var terminalDirectives = [
  12. 'repeat',
  13. 'if'
  14. ]
  15. /**
  16. * Compile a template and return a reusable composite link
  17. * function, which recursively contains more link functions
  18. * inside. This top level compile function would normally
  19. * be called on instance root nodes, but can also be used
  20. * for partial compilation if the partial argument is true.
  21. *
  22. * The returned composite link function, when called, will
  23. * return an unlink function that tearsdown all directives
  24. * created during the linking phase.
  25. *
  26. * @param {Element|DocumentFragment} el
  27. * @param {Object} options
  28. * @param {Boolean} partial
  29. * @param {Vue} [host] - host vm of transcluded content
  30. * @return {Function}
  31. */
  32. exports.compile = function (el, options, partial, host) {
  33. // link function for the node itself.
  34. var nodeLinkFn = partial || !options._asComponent
  35. ? compileNode(el, options)
  36. : null
  37. // link function for the childNodes
  38. var childLinkFn =
  39. !(nodeLinkFn && nodeLinkFn.terminal) &&
  40. el.tagName !== 'SCRIPT' &&
  41. el.hasChildNodes()
  42. ? compileNodeList(el.childNodes, options)
  43. : null
  44. /**
  45. * A composite linker function to be called on a already
  46. * compiled piece of DOM, which instantiates all directive
  47. * instances.
  48. *
  49. * @param {Vue} vm
  50. * @param {Element|DocumentFragment} el
  51. * @return {Function|undefined}
  52. */
  53. return function compositeLinkFn (vm, el) {
  54. // cache childNodes before linking parent, fix #657
  55. var childNodes = _.toArray(el.childNodes)
  56. // link
  57. var dirs = linkAndCapture(function () {
  58. if (nodeLinkFn) nodeLinkFn(vm, el, host)
  59. if (childLinkFn) childLinkFn(vm, childNodes, host)
  60. }, vm)
  61. /**
  62. * The linker function returns an unlink function that
  63. * tearsdown all directives instances generated during
  64. * the process.
  65. *
  66. * @param {Boolean} destroying
  67. */
  68. return function unlink (destroying) {
  69. teardownDirs(vm, dirs, destroying)
  70. }
  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. return vm._directives.slice(originalDirCount)
  84. }
  85. /**
  86. * Teardown partial linked directives.
  87. *
  88. * @param {Vue} vm
  89. * @param {Array} dirs
  90. * @param {Boolean} destroying
  91. */
  92. function teardownDirs (vm, dirs, destroying) {
  93. if (!dirs) return
  94. var i = dirs.length
  95. while (i--) {
  96. dirs[i]._teardown()
  97. if (!destroying) {
  98. vm._directives.$remove(dirs[i])
  99. }
  100. }
  101. }
  102. /**
  103. * Compile the root element of an instance. There are
  104. * 3 types of things to process here:
  105. *
  106. * 1. props on parent container (child scope)
  107. * 2. other attrs on parent container (parent scope)
  108. * 3. attrs on the component template root node, if
  109. * replace:true (child scope)
  110. *
  111. * Also, if this is a block instance, we only need to
  112. * compile 1 & 2 here.
  113. *
  114. * This function does compile and link at the same time,
  115. * since root linkers can not be reused. It returns the
  116. * unlink function for potential parent directives on the
  117. * container.
  118. *
  119. * @param {Vue} vm
  120. * @param {Element} el
  121. * @param {Object} options
  122. * @return {Function}
  123. */
  124. exports.compileAndLinkRoot = function (vm, el, options) {
  125. var containerAttrs = options._containerAttrs
  126. var replacerAttrs = options._replacerAttrs
  127. var props = options.props
  128. var propsLinkFn, parentLinkFn, replacerLinkFn
  129. // 1. props
  130. propsLinkFn = props && containerAttrs
  131. ? compileProps(el, containerAttrs, props)
  132. : null
  133. // only need to compile other attributes for
  134. // non-block instances
  135. if (el.nodeType !== 11) {
  136. // for components, container and replacer need to be
  137. // compiled separately and linked in different scopes.
  138. if (options._asComponent) {
  139. // 2. container attributes
  140. if (containerAttrs) {
  141. parentLinkFn = compileDirectives(containerAttrs, options)
  142. }
  143. if (replacerAttrs) {
  144. // 3. replacer attributes
  145. replacerLinkFn = compileDirectives(replacerAttrs, options)
  146. }
  147. } else {
  148. // non-component, just compile as a normal element.
  149. replacerLinkFn = compileDirectives(el, options)
  150. }
  151. }
  152. // link parent dirs
  153. var parent = vm.$parent
  154. var parentDirs
  155. if (parent && parentLinkFn) {
  156. parentDirs = linkAndCapture(function () {
  157. parentLinkFn(parent, el)
  158. }, parent)
  159. }
  160. // link self
  161. var selfDirs = linkAndCapture(function () {
  162. if (propsLinkFn) propsLinkFn(vm, null)
  163. if (replacerLinkFn) replacerLinkFn(vm, el)
  164. }, vm)
  165. // return the unlink function that tearsdown parent
  166. // container directives.
  167. return function rootUnlinkFn () {
  168. teardownDirs(parent, parentDirs)
  169. teardownDirs(vm, selfDirs)
  170. }
  171. }
  172. /**
  173. * Compile a node and return a nodeLinkFn based on the
  174. * node type.
  175. *
  176. * @param {Node} node
  177. * @param {Object} options
  178. * @return {Function|null}
  179. */
  180. function compileNode (node, options) {
  181. var type = node.nodeType
  182. if (type === 1 && node.tagName !== 'SCRIPT') {
  183. return compileElement(node, options)
  184. } else if (type === 3 && config.interpolate && node.data.trim()) {
  185. return compileTextNode(node, options)
  186. } else {
  187. return null
  188. }
  189. }
  190. /**
  191. * Compile an element and return a nodeLinkFn.
  192. *
  193. * @param {Element} el
  194. * @param {Object} options
  195. * @return {Function|null}
  196. */
  197. function compileElement (el, options) {
  198. var hasAttrs = el.hasAttributes()
  199. // check element directives
  200. var linkFn = checkElementDirectives(el, options)
  201. // check terminal directives (repeat & if)
  202. if (!linkFn && hasAttrs) {
  203. linkFn = checkTerminalDirectives(el, options)
  204. }
  205. // check component
  206. if (!linkFn) {
  207. linkFn = checkComponent(el, options)
  208. }
  209. // normal directives
  210. if (!linkFn && hasAttrs) {
  211. linkFn = compileDirectives(el, options)
  212. }
  213. // if the element is a textarea, we need to interpolate
  214. // its content on initial render.
  215. if (el.tagName === 'TEXTAREA') {
  216. var realLinkFn = linkFn
  217. linkFn = function (vm, el) {
  218. el.value = vm.$interpolate(el.value)
  219. if (realLinkFn) realLinkFn(vm, el)
  220. }
  221. linkFn.terminal = true
  222. }
  223. return linkFn
  224. }
  225. /**
  226. * Compile a textNode and return a nodeLinkFn.
  227. *
  228. * @param {TextNode} node
  229. * @param {Object} options
  230. * @return {Function|null} textNodeLinkFn
  231. */
  232. function compileTextNode (node, options) {
  233. var tokens = textParser.parse(node.data)
  234. if (!tokens) {
  235. return null
  236. }
  237. var frag = document.createDocumentFragment()
  238. var el, token
  239. for (var i = 0, l = tokens.length; i < l; i++) {
  240. token = tokens[i]
  241. el = token.tag
  242. ? processTextToken(token, options)
  243. : document.createTextNode(token.value)
  244. frag.appendChild(el)
  245. }
  246. return makeTextNodeLinkFn(tokens, frag, options)
  247. }
  248. /**
  249. * Process a single text token.
  250. *
  251. * @param {Object} token
  252. * @param {Object} options
  253. * @return {Node}
  254. */
  255. function processTextToken (token, options) {
  256. var el
  257. if (token.oneTime) {
  258. el = document.createTextNode(token.value)
  259. } else {
  260. if (token.html) {
  261. el = document.createComment('v-html')
  262. setTokenType('html')
  263. } else {
  264. // IE will clean up empty textNodes during
  265. // frag.cloneNode(true), so we have to give it
  266. // something here...
  267. el = document.createTextNode(' ')
  268. setTokenType('text')
  269. }
  270. }
  271. function setTokenType (type) {
  272. token.type = type
  273. token.def = resolveAsset(options, 'directives', type)
  274. token.descriptor = dirParser.parse(token.value)[0]
  275. }
  276. return el
  277. }
  278. /**
  279. * Build a function that processes a textNode.
  280. *
  281. * @param {Array<Object>} tokens
  282. * @param {DocumentFragment} frag
  283. */
  284. function makeTextNodeLinkFn (tokens, frag) {
  285. return function textNodeLinkFn (vm, el) {
  286. var fragClone = frag.cloneNode(true)
  287. var childNodes = _.toArray(fragClone.childNodes)
  288. var token, value, node
  289. for (var i = 0, l = tokens.length; i < l; i++) {
  290. token = tokens[i]
  291. value = token.value
  292. if (token.tag) {
  293. node = childNodes[i]
  294. if (token.oneTime) {
  295. value = vm.$eval(value)
  296. if (token.html) {
  297. _.replace(node, templateParser.parse(value, true))
  298. } else {
  299. node.data = value
  300. }
  301. } else {
  302. vm._bindDir(token.type, node,
  303. token.descriptor, token.def)
  304. }
  305. }
  306. }
  307. _.replace(el, fragClone)
  308. }
  309. }
  310. /**
  311. * Compile a node list and return a childLinkFn.
  312. *
  313. * @param {NodeList} nodeList
  314. * @param {Object} options
  315. * @return {Function|undefined}
  316. */
  317. function compileNodeList (nodeList, options) {
  318. var linkFns = []
  319. var nodeLinkFn, childLinkFn, node
  320. for (var i = 0, l = nodeList.length; i < l; i++) {
  321. node = nodeList[i]
  322. nodeLinkFn = compileNode(node, options)
  323. childLinkFn =
  324. !(nodeLinkFn && nodeLinkFn.terminal) &&
  325. node.tagName !== 'SCRIPT' &&
  326. node.hasChildNodes()
  327. ? compileNodeList(node.childNodes, options)
  328. : null
  329. linkFns.push(nodeLinkFn, childLinkFn)
  330. }
  331. return linkFns.length
  332. ? makeChildLinkFn(linkFns)
  333. : null
  334. }
  335. /**
  336. * Make a child link function for a node's childNodes.
  337. *
  338. * @param {Array<Function>} linkFns
  339. * @return {Function} childLinkFn
  340. */
  341. function makeChildLinkFn (linkFns) {
  342. return function childLinkFn (vm, nodes, host) {
  343. var node, nodeLinkFn, childrenLinkFn
  344. for (var i = 0, n = 0, l = linkFns.length; i < l; n++) {
  345. node = nodes[n]
  346. nodeLinkFn = linkFns[i++]
  347. childrenLinkFn = linkFns[i++]
  348. // cache childNodes before linking parent, fix #657
  349. var childNodes = _.toArray(node.childNodes)
  350. if (nodeLinkFn) {
  351. nodeLinkFn(vm, node, host)
  352. }
  353. if (childrenLinkFn) {
  354. childrenLinkFn(vm, childNodes, host)
  355. }
  356. }
  357. }
  358. }
  359. /**
  360. * Compile param attributes on a root element and return
  361. * a props link function.
  362. *
  363. * @param {Element|DocumentFragment} el
  364. * @param {Object} attrs
  365. * @param {Array} propNames
  366. * @return {Function} propsLinkFn
  367. */
  368. var dataAttrRE = /^data-/
  369. var settablePathRE = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*|\[[^\[\]]+\])*$/
  370. var literalValueRE = /^true|false|\d+$/
  371. var identRE = require('../parsers/path').identRE
  372. function compileProps (el, attrs, propNames) {
  373. var props = []
  374. var i = propNames.length
  375. var name, value, path, prop, settable, single
  376. while (i--) {
  377. name = propNames[i]
  378. // props could contain dashes, which will be
  379. // interpreted as minus calculations by the parser
  380. // so we need to camelize the path here
  381. path = _.camelize(name.replace(dataAttrRE, ''))
  382. if (/[A-Z]/.test(name)) {
  383. _.warn(
  384. 'You seem to be using camelCase for a component prop, ' +
  385. 'but HTML doesn\'t differentiate between upper and ' +
  386. 'lower case. You should use hyphen-delimited ' +
  387. 'attribute names. For more info see ' +
  388. 'http://vuejs.org/api/options.html#props'
  389. )
  390. }
  391. if (!identRE.test(path)) {
  392. _.warn(
  393. 'Invalid prop key: "' + name + '". Prop keys ' +
  394. 'must be valid identifiers.'
  395. )
  396. }
  397. value = attrs[name]
  398. /* jshint eqeqeq:false */
  399. if (value != null) {
  400. prop = {
  401. name: name,
  402. raw: value,
  403. path: path
  404. }
  405. var tokens = textParser.parse(value)
  406. if (tokens) {
  407. if (el && el.nodeType === 1) {
  408. el.removeAttribute(name)
  409. }
  410. // important so that this doesn't get compiled
  411. // again as a normal attribute binding
  412. attrs[name] = null
  413. prop.dynamic = true
  414. prop.parentPath = textParser.tokensToExp(tokens)
  415. // check prop binding type.
  416. single = tokens.length === 1
  417. settable =
  418. settablePathRE.test(prop.parentPath) &&
  419. !literalValueRE.test(prop.parentPath)
  420. // one time: {{* prop}}
  421. prop.oneTime =
  422. !single ||
  423. !settable ||
  424. tokens[0].oneTime
  425. // one way down: {{> prop}}
  426. prop.oneWayDown =
  427. single &&
  428. tokens[0].oneWay === 62 // >
  429. // one way up: {{< prop}}
  430. prop.oneWayUp =
  431. tokens[0].oneWay === 60 && // <
  432. settable
  433. }
  434. props.push(prop)
  435. }
  436. }
  437. return makePropsLinkFn(props)
  438. }
  439. /**
  440. * Build a function that applies props to a vm.
  441. *
  442. * @param {Array} props
  443. * @return {Function} propsLinkFn
  444. */
  445. function makePropsLinkFn (props) {
  446. return function propsLinkFn (vm, el) {
  447. var i = props.length
  448. var prop, path
  449. while (i--) {
  450. prop = props[i]
  451. path = prop.path
  452. if (prop.dynamic) {
  453. if (vm.$parent) {
  454. if (prop.onetime) {
  455. // one time binding
  456. vm.$set(path, vm.$parent.$get(prop.parentPath))
  457. } else {
  458. // dynamic binding
  459. vm._bindDir('prop', el, prop, propDef)
  460. }
  461. } else {
  462. _.warn(
  463. 'Cannot bind dynamic prop on a root instance' +
  464. ' with no parent: ' + prop.name + '="' +
  465. prop.raw + '"'
  466. )
  467. }
  468. } else {
  469. // literal, just set once
  470. vm.$set(path, _.toNumber(prop.raw))
  471. }
  472. }
  473. }
  474. }
  475. /**
  476. * Check for element directives (custom elements that should
  477. * be resovled as terminal directives).
  478. *
  479. * @param {Element} el
  480. * @param {Object} options
  481. */
  482. function checkElementDirectives (el, options) {
  483. var tag = el.tagName.toLowerCase()
  484. var def = resolveAsset(options, 'elementDirectives', tag)
  485. if (def) {
  486. return makeTerminalNodeLinkFn(el, tag, '', options, def)
  487. }
  488. }
  489. /**
  490. * Check if an element is a component. If yes, return
  491. * a component link function.
  492. *
  493. * @param {Element} el
  494. * @param {Object} options
  495. * @return {Function|undefined}
  496. */
  497. function checkComponent (el, options) {
  498. var componentId = _.checkComponent(el, options)
  499. if (componentId) {
  500. var componentLinkFn = function (vm, el, host) {
  501. vm._bindDir('component', el, {
  502. expression: componentId
  503. }, componentDef, host)
  504. }
  505. componentLinkFn.terminal = true
  506. return componentLinkFn
  507. }
  508. }
  509. /**
  510. * Check an element for terminal directives in fixed order.
  511. * If it finds one, return a terminal link function.
  512. *
  513. * @param {Element} el
  514. * @param {Object} options
  515. * @return {Function} terminalLinkFn
  516. */
  517. function checkTerminalDirectives (el, options) {
  518. if (_.attr(el, 'pre') !== null) {
  519. return skip
  520. }
  521. var value, dirName
  522. /* jshint boss: true */
  523. for (var i = 0, l = terminalDirectives.length; i < l; i++) {
  524. dirName = terminalDirectives[i]
  525. if ((value = _.attr(el, dirName)) !== null) {
  526. return makeTerminalNodeLinkFn(el, dirName, value, options)
  527. }
  528. }
  529. }
  530. function skip () {}
  531. skip.terminal = true
  532. /**
  533. * Build a node link function for a terminal directive.
  534. * A terminal link function terminates the current
  535. * compilation recursion and handles compilation of the
  536. * subtree in the directive.
  537. *
  538. * @param {Element} el
  539. * @param {String} dirName
  540. * @param {String} value
  541. * @param {Object} options
  542. * @param {Object} [def]
  543. * @return {Function} terminalLinkFn
  544. */
  545. function makeTerminalNodeLinkFn (el, dirName, value, options, def) {
  546. var descriptor = dirParser.parse(value)[0]
  547. // no need to call resolveAsset since terminal directives
  548. // are always internal
  549. def = def || options.directives[dirName]
  550. var fn = function terminalNodeLinkFn (vm, el, host) {
  551. vm._bindDir(dirName, el, descriptor, def, host)
  552. }
  553. fn.terminal = true
  554. return fn
  555. }
  556. /**
  557. * Compile the directives on an element and return a linker.
  558. *
  559. * @param {Element|Object} elOrAttrs
  560. * - could be an object of already-extracted
  561. * container attributes.
  562. * @param {Object} options
  563. * @return {Function}
  564. */
  565. function compileDirectives (elOrAttrs, options) {
  566. var attrs = _.isPlainObject(elOrAttrs)
  567. ? mapToList(elOrAttrs)
  568. : elOrAttrs.attributes
  569. var i = attrs.length
  570. var dirs = []
  571. var attr, name, value, dir, dirName, dirDef
  572. while (i--) {
  573. attr = attrs[i]
  574. name = attr.name
  575. value = attr.value
  576. if (value === null) continue
  577. if (name.indexOf(config.prefix) === 0) {
  578. dirName = name.slice(config.prefix.length)
  579. dirDef = resolveAsset(options, 'directives', dirName)
  580. _.assertAsset(dirDef, 'directive', dirName)
  581. if (dirDef) {
  582. dirs.push({
  583. name: dirName,
  584. descriptors: dirParser.parse(value),
  585. def: dirDef
  586. })
  587. }
  588. } else if (config.interpolate) {
  589. dir = collectAttrDirective(name, value, options)
  590. if (dir) {
  591. dirs.push(dir)
  592. }
  593. }
  594. }
  595. // sort by priority, LOW to HIGH
  596. if (dirs.length) {
  597. dirs.sort(directiveComparator)
  598. return makeNodeLinkFn(dirs)
  599. }
  600. }
  601. /**
  602. * Convert a map (Object) of attributes to an Array.
  603. *
  604. * @param {Object} map
  605. * @return {Array}
  606. */
  607. function mapToList (map) {
  608. var list = []
  609. for (var key in map) {
  610. list.push({
  611. name: key,
  612. value: map[key]
  613. })
  614. }
  615. return list
  616. }
  617. /**
  618. * Build a link function for all directives on a single node.
  619. *
  620. * @param {Array} directives
  621. * @return {Function} directivesLinkFn
  622. */
  623. function makeNodeLinkFn (directives) {
  624. return function nodeLinkFn (vm, el, host) {
  625. // reverse apply because it's sorted low to high
  626. var i = directives.length
  627. var dir, j, k
  628. while (i--) {
  629. dir = directives[i]
  630. if (dir._link) {
  631. // custom link fn
  632. dir._link(vm, el)
  633. } else {
  634. k = dir.descriptors.length
  635. for (j = 0; j < k; j++) {
  636. vm._bindDir(dir.name, el,
  637. dir.descriptors[j], dir.def, host)
  638. }
  639. }
  640. }
  641. }
  642. }
  643. /**
  644. * Check an attribute for potential dynamic bindings,
  645. * and return a directive object.
  646. *
  647. * @param {String} name
  648. * @param {String} value
  649. * @param {Object} options
  650. * @return {Object}
  651. */
  652. function collectAttrDirective (name, value, options) {
  653. var tokens = textParser.parse(value)
  654. if (tokens) {
  655. var def = options.directives.attr
  656. var i = tokens.length
  657. var allOneTime = true
  658. while (i--) {
  659. var token = tokens[i]
  660. if (token.tag && !token.oneTime) {
  661. allOneTime = false
  662. }
  663. }
  664. return {
  665. def: def,
  666. _link: allOneTime
  667. ? function (vm, el) {
  668. el.setAttribute(name, vm.$interpolate(value))
  669. }
  670. : function (vm, el) {
  671. var value = textParser.tokensToExp(tokens, vm)
  672. var desc = dirParser.parse(name + ':' + value)[0]
  673. vm._bindDir('attr', el, desc, def)
  674. }
  675. }
  676. }
  677. }
  678. /**
  679. * Directive priority sort comparator
  680. *
  681. * @param {Object} a
  682. * @param {Object} b
  683. */
  684. function directiveComparator (a, b) {
  685. a = a.def.priority || 0
  686. b = b.def.priority || 0
  687. return a > b ? 1 : -1
  688. }