compile.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. var _ = require('../util')
  2. var config = require('../config')
  3. var textParser = require('../parse/text')
  4. var dirParser = require('../parse/directive')
  5. var templateParser = require('../parse/template')
  6. /**
  7. * Compile a template and return a reusable composite link
  8. * function, which recursively contains more link functions
  9. * inside. This top level compile function should only be
  10. * called on instance root nodes.
  11. *
  12. * @param {Element|DocumentFragment} el
  13. * @param {Object} options
  14. * @param {Boolean} partial
  15. * @return {Function}
  16. */
  17. module.exports = function compile (el, options, partial) {
  18. var params = !partial && options.paramAttributes
  19. var paramsLinkFn = params
  20. ? compileParamAttributes(el, params, options)
  21. : null
  22. var nodeLinkFn = el instanceof DocumentFragment
  23. ? null
  24. : compileNode(el, options)
  25. var childLinkFn =
  26. (!nodeLinkFn || !nodeLinkFn.terminal) &&
  27. el.hasChildNodes()
  28. ? compileNodeList(el.childNodes, options)
  29. : null
  30. /**
  31. * A linker function to be called on a already compiled
  32. * piece of DOM, which instantiates all directive
  33. * instances.
  34. *
  35. * @param {Vue} vm
  36. * @param {Element|DocumentFragment} el
  37. * @return {Function|undefined}
  38. */
  39. return function link (vm, el) {
  40. var originalDirCount = vm._directives.length
  41. if (paramsLinkFn) paramsLinkFn(vm, el)
  42. if (nodeLinkFn) nodeLinkFn(vm, el)
  43. if (childLinkFn) childLinkFn(vm, el.childNodes)
  44. /**
  45. * If this is a partial compile, the linker function
  46. * returns an unlink function that tearsdown all
  47. * directives instances generated during the partial
  48. * linking.
  49. */
  50. if (partial) {
  51. var dirs = vm._directives.slice(originalDirCount)
  52. return function unlink () {
  53. var i = dirs.length
  54. while (i--) {
  55. dirs[i]._teardown()
  56. }
  57. i = vm._directives.indexOf(dirs[0])
  58. vm._directives.splice(i, dirs.length)
  59. }
  60. }
  61. }
  62. }
  63. /**
  64. * Compile a node and return a nodeLinkFn based on the
  65. * node type.
  66. *
  67. * @param {Node} node
  68. * @param {Object} options
  69. * @return {Function|undefined}
  70. */
  71. function compileNode (node, options) {
  72. var type = node.nodeType
  73. if (type === 1 && node.tagName !== 'SCRIPT') {
  74. return compileElement(node, options)
  75. } else if (type === 3 && config.interpolate) {
  76. return compileTextNode(node, options)
  77. }
  78. }
  79. /**
  80. * Compile an element and return a nodeLinkFn.
  81. *
  82. * @param {Element} el
  83. * @param {Object} options
  84. * @return {Function|null}
  85. */
  86. function compileElement (el, options) {
  87. var linkFn, tag, component
  88. // check custom element component, but only on non-root
  89. if (!el.__vue__) {
  90. tag = el.tagName.toLowerCase()
  91. component =
  92. tag.indexOf('-') > 0 &&
  93. options.components[tag]
  94. if (component) {
  95. el.setAttribute(config.prefix + 'component', tag)
  96. }
  97. }
  98. if (component || el.hasAttributes()) {
  99. // check terminal direcitves
  100. linkFn = checkTerminalDirectives(el, options)
  101. // if not terminal, build normal link function
  102. if (!linkFn) {
  103. var directives = collectDirectives(el, options)
  104. linkFn = directives.length
  105. ? makeDirectivesLinkFn(directives)
  106. : null
  107. }
  108. }
  109. // if the element is a textarea, we need to interpolate
  110. // its content on initial render.
  111. if (el.tagName === 'TEXTAREA') {
  112. var realLinkFn = linkFn
  113. linkFn = function (vm, el) {
  114. el.value = vm.$interpolate(el.value)
  115. if (realLinkFn) realLinkFn(vm, el)
  116. }
  117. linkFn.terminal = true
  118. }
  119. return linkFn
  120. }
  121. /**
  122. * Build a multi-directive link function.
  123. *
  124. * @param {Array} directives
  125. * @return {Function} directivesLinkFn
  126. */
  127. function makeDirectivesLinkFn (directives) {
  128. return function directivesLinkFn (vm, el) {
  129. // reverse apply because it's sorted low to high
  130. var i = directives.length
  131. var dir, j, k
  132. while (i--) {
  133. dir = directives[i]
  134. if (dir._link) {
  135. // custom link fn
  136. dir._link(vm, el)
  137. } else {
  138. k = dir.descriptors.length
  139. for (j = 0; j < k; j++) {
  140. vm._bindDir(dir.name, el,
  141. dir.descriptors[j], dir.def)
  142. }
  143. }
  144. }
  145. }
  146. }
  147. /**
  148. * Compile a textNode and return a nodeLinkFn.
  149. *
  150. * @param {TextNode} node
  151. * @param {Object} options
  152. * @return {Function|null} textNodeLinkFn
  153. */
  154. function compileTextNode (node, options) {
  155. var tokens = textParser.parse(node.nodeValue)
  156. if (!tokens) {
  157. return null
  158. }
  159. var frag = document.createDocumentFragment()
  160. var dirs = options.directives
  161. var el, token, value
  162. for (var i = 0, l = tokens.length; i < l; i++) {
  163. token = tokens[i]
  164. value = token.value
  165. if (token.tag) {
  166. if (token.oneTime) {
  167. el = document.createTextNode(value)
  168. } else {
  169. if (token.html) {
  170. el = document.createComment('v-html')
  171. token.type = 'html'
  172. token.def = dirs.html
  173. token.descriptor = dirParser.parse(value)[0]
  174. } else if (token.partial) {
  175. el = document.createComment('v-partial')
  176. token.type = 'partial'
  177. token.def = dirs.partial
  178. token.descriptor = dirParser.parse(value)[0]
  179. } else {
  180. // IE will clean up empty textNodes during
  181. // frag.cloneNode(true), so we have to give it
  182. // something here...
  183. el = document.createTextNode(' ')
  184. token.type = 'text'
  185. token.def = dirs.text
  186. token.descriptor = dirParser.parse(value)[0]
  187. }
  188. }
  189. } else {
  190. el = document.createTextNode(value)
  191. }
  192. frag.appendChild(el)
  193. }
  194. return makeTextNodeLinkFn(tokens, frag, options)
  195. }
  196. /**
  197. * Build a function that processes a textNode.
  198. *
  199. * @param {Array<Object>} tokens
  200. * @param {DocumentFragment} frag
  201. */
  202. function makeTextNodeLinkFn (tokens, frag) {
  203. return function textNodeLinkFn (vm, el) {
  204. var fragClone = frag.cloneNode(true)
  205. var childNodes = _.toArray(fragClone.childNodes)
  206. var token, value, node
  207. for (var i = 0, l = tokens.length; i < l; i++) {
  208. token = tokens[i]
  209. value = token.value
  210. if (token.tag) {
  211. node = childNodes[i]
  212. if (token.oneTime) {
  213. value = vm.$eval(value)
  214. if (token.html) {
  215. _.replace(node, templateParser.parse(value, true))
  216. } else {
  217. node.nodeValue = value
  218. }
  219. } else {
  220. vm._bindDir(token.type, node,
  221. token.descriptor, token.def)
  222. }
  223. }
  224. }
  225. _.replace(el, fragClone)
  226. }
  227. }
  228. /**
  229. * Compile a node list and return a childLinkFn.
  230. *
  231. * @param {NodeList} nodeList
  232. * @param {Object} options
  233. * @return {Function|undefined}
  234. */
  235. function compileNodeList (nodeList, options) {
  236. var linkFns = []
  237. var nodeLinkFn, childLinkFn, node
  238. for (var i = 0, l = nodeList.length; i < l; i++) {
  239. node = nodeList[i]
  240. nodeLinkFn = compileNode(node, options)
  241. childLinkFn =
  242. (!nodeLinkFn || !nodeLinkFn.terminal) &&
  243. node.hasChildNodes()
  244. ? compileNodeList(node.childNodes, options)
  245. : null
  246. linkFns.push(nodeLinkFn, childLinkFn)
  247. }
  248. return linkFns.length
  249. ? makeChildLinkFn(linkFns)
  250. : null
  251. }
  252. /**
  253. * Make a child link function for a node's childNodes.
  254. *
  255. * @param {Array<Function>} linkFns
  256. * @return {Function} childLinkFn
  257. */
  258. function makeChildLinkFn (linkFns) {
  259. return function childLinkFn (vm, nodes) {
  260. // stablize nodes
  261. nodes = _.toArray(nodes)
  262. var node, nodeLinkFn, childrenLinkFn
  263. for (var i = 0, n = 0, l = linkFns.length; i < l; n++) {
  264. node = nodes[n]
  265. nodeLinkFn = linkFns[i++]
  266. childrenLinkFn = linkFns[i++]
  267. if (nodeLinkFn) {
  268. nodeLinkFn(vm, node)
  269. }
  270. if (childrenLinkFn) {
  271. childrenLinkFn(vm, node.childNodes)
  272. }
  273. }
  274. }
  275. }
  276. /**
  277. * Compile param attributes on a root element and return
  278. * a paramAttributes link function.
  279. *
  280. * @param {Element} el
  281. * @param {Array} attrs
  282. * @param {Object} options
  283. * @return {Function} paramsLinkFn
  284. */
  285. function compileParamAttributes (el, attrs, options) {
  286. var params = []
  287. var i = attrs.length
  288. var name, value, param
  289. while (i--) {
  290. name = attrs[i]
  291. value = el.getAttribute(name)
  292. if (value !== null) {
  293. param = {
  294. name: name,
  295. value: value
  296. }
  297. var tokens = textParser.parse(value)
  298. if (tokens) {
  299. el.removeAttribute(name)
  300. if (tokens.length > 1) {
  301. _.warn(
  302. 'Invalid param attribute binding: "' +
  303. name + '="' + value + '"' +
  304. '\nDon\'t mix binding tags with plain text ' +
  305. 'in param attribute bindings.'
  306. )
  307. continue
  308. } else {
  309. param.dynamic = true
  310. param.value = tokens[0].value
  311. }
  312. }
  313. params.push(param)
  314. }
  315. }
  316. return makeParamsLinkFn(params, options)
  317. }
  318. /**
  319. * Build a function that applies param attributes to a vm.
  320. *
  321. * @param {Array} params
  322. * @param {Object} options
  323. * @return {Function} paramsLinkFn
  324. */
  325. function makeParamsLinkFn (params, options) {
  326. var def = options.directives['with']
  327. return function paramsLinkFn (vm, el) {
  328. var i = params.length
  329. var param
  330. while (i--) {
  331. param = params[i]
  332. if (param.dynamic) {
  333. // dynamic param attribtues are bound as v-with.
  334. // we can directly duck the descriptor here beacuse
  335. // param attributes cannot use expressions or
  336. // filters.
  337. vm._bindDir('with', el, {
  338. arg: param.name,
  339. expression: param.value
  340. }, def)
  341. } else {
  342. // just set once
  343. vm.$set(param.name, param.value)
  344. }
  345. }
  346. }
  347. }
  348. /**
  349. * Check an element for terminal directives in fixed order.
  350. * If it finds one, return a terminal link function.
  351. *
  352. * @param {Element} el
  353. * @param {Object} options
  354. * @return {Function} terminalLinkFn
  355. */
  356. var terminalDirectives = [
  357. 'repeat',
  358. 'if',
  359. 'component'
  360. ]
  361. function skip () {}
  362. skip.terminal = true
  363. function checkTerminalDirectives (el, options) {
  364. if (_.attr(el, 'pre') !== null) {
  365. return skip
  366. }
  367. var value, dirName
  368. /* jshint boss: true */
  369. for (var i = 0; i < 3; i++) {
  370. dirName = terminalDirectives[i]
  371. if (value = _.attr(el, dirName)) {
  372. return makeTeriminalLinkFn(el, dirName, value, options)
  373. }
  374. }
  375. }
  376. /**
  377. * Build a link function for a terminal directive.
  378. *
  379. * @param {Element} el
  380. * @param {String} dirName
  381. * @param {String} value
  382. * @param {Object} options
  383. * @return {Function} terminalLinkFn
  384. */
  385. function makeTeriminalLinkFn (el, dirName, value, options) {
  386. var descriptor = dirParser.parse(value)[0]
  387. var def = options.directives[dirName]
  388. var terminalLinkFn = function (vm, el) {
  389. vm._bindDir(dirName, el, descriptor, def)
  390. }
  391. terminalLinkFn.terminal = true
  392. return terminalLinkFn
  393. }
  394. /**
  395. * Collect the directives on an element.
  396. *
  397. * @param {Element} el
  398. * @param {Object} options
  399. * @return {Array}
  400. */
  401. function collectDirectives (el, options) {
  402. var attrs = _.toArray(el.attributes)
  403. var i = attrs.length
  404. var dirs = []
  405. var attr, attrName, dir, dirName, dirDef
  406. while (i--) {
  407. attr = attrs[i]
  408. attrName = attr.name
  409. if (attrName.indexOf(config.prefix) === 0) {
  410. dirName = attrName.slice(config.prefix.length)
  411. dirDef = options.directives[dirName]
  412. _.assertAsset(dirDef, 'directive', dirName)
  413. if (dirDef) {
  414. if (dirName !== 'cloak') {
  415. el.removeAttribute(attrName)
  416. }
  417. dirs.push({
  418. name: dirName,
  419. descriptors: dirParser.parse(attr.value),
  420. def: dirDef
  421. })
  422. }
  423. } else if (config.interpolate) {
  424. dir = collectAttrDirective(el, attrName, attr.value,
  425. options)
  426. if (dir) {
  427. dirs.push(dir)
  428. }
  429. }
  430. }
  431. // sort by priority, LOW to HIGH
  432. dirs.sort(directiveComparator)
  433. return dirs
  434. }
  435. /**
  436. * Check an attribute for potential dynamic bindings,
  437. * and return a directive object.
  438. *
  439. * @param {Element} el
  440. * @param {String} name
  441. * @param {String} value
  442. * @param {Object} options
  443. * @return {Object}
  444. */
  445. function collectAttrDirective (el, name, value, options) {
  446. var tokens = textParser.parse(value)
  447. if (tokens) {
  448. var def = options.directives.attr
  449. var i = tokens.length
  450. var allOneTime = true
  451. while (i--) {
  452. var token = tokens[i]
  453. if (token.tag && !token.oneTime) {
  454. allOneTime = false
  455. }
  456. }
  457. return {
  458. def: def,
  459. _link: allOneTime
  460. ? function (vm, el) {
  461. el.setAttribute(name, vm.$interpolate(value))
  462. }
  463. : function (vm, el) {
  464. var value = textParser.tokensToExp(tokens, vm)
  465. var desc = dirParser.parse(name + ':' + value)[0]
  466. vm._bindDir('attr', el, desc, def)
  467. }
  468. }
  469. }
  470. }
  471. /**
  472. * Directive priority sort comparator
  473. *
  474. * @param {Object} a
  475. * @param {Object} b
  476. */
  477. function directiveComparator (a, b) {
  478. a = a.def.priority || 0
  479. b = b.def.priority || 0
  480. return a > b ? 1 : -1
  481. }