compile.js 12 KB

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