templateTransformSrcset.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. import path from 'path'
  2. import {
  3. ConstantTypes,
  4. createCompoundExpression,
  5. createSimpleExpression,
  6. ExpressionNode,
  7. NodeTransform,
  8. NodeTypes,
  9. SimpleExpressionNode
  10. } from '@vue/compiler-core'
  11. import {
  12. isRelativeUrl,
  13. parseUrl,
  14. isExternalUrl,
  15. isDataUrl
  16. } from './templateUtils'
  17. import {
  18. AssetURLOptions,
  19. defaultAssetUrlOptions
  20. } from './templateTransformAssetUrl'
  21. const srcsetTags = ['img', 'source']
  22. interface ImageCandidate {
  23. url: string
  24. descriptor: string
  25. }
  26. // http://w3c.github.io/html/semantics-embedded-content.html#ref-for-image-candidate-string-5
  27. const escapedSpaceCharacters = /( |\\t|\\n|\\f|\\r)+/g
  28. export const createSrcsetTransformWithOptions = (
  29. options: Required<AssetURLOptions>
  30. ): NodeTransform => {
  31. return (node, context) =>
  32. (transformSrcset as Function)(node, context, options)
  33. }
  34. export const transformSrcset: NodeTransform = (
  35. node,
  36. context,
  37. options: Required<AssetURLOptions> = defaultAssetUrlOptions
  38. ) => {
  39. if (node.type === NodeTypes.ELEMENT) {
  40. if (srcsetTags.includes(node.tag) && node.props.length) {
  41. node.props.forEach((attr, index) => {
  42. if (attr.name === 'srcset' && attr.type === NodeTypes.ATTRIBUTE) {
  43. if (!attr.value) return
  44. const value = attr.value.content
  45. if (!value) return
  46. const imageCandidates: ImageCandidate[] = value.split(',').map(s => {
  47. // The attribute value arrives here with all whitespace, except
  48. // normal spaces, represented by escape sequences
  49. const [url, descriptor] = s
  50. .replace(escapedSpaceCharacters, ' ')
  51. .trim()
  52. .split(' ', 2)
  53. return { url, descriptor }
  54. })
  55. // data urls contains comma after the encoding so we need to re-merge
  56. // them
  57. for (let i = 0; i < imageCandidates.length; i++) {
  58. const { url } = imageCandidates[i]
  59. if (isDataUrl(url)) {
  60. imageCandidates[i + 1].url =
  61. url + ',' + imageCandidates[i + 1].url
  62. imageCandidates.splice(i, 1)
  63. }
  64. }
  65. const shouldProcessUrl = (url: string) => {
  66. return (
  67. !isExternalUrl(url) &&
  68. !isDataUrl(url) &&
  69. (options.includeAbsolute || isRelativeUrl(url))
  70. )
  71. }
  72. // When srcset does not contain any qualified URLs, skip transforming
  73. if (!imageCandidates.some(({ url }) => shouldProcessUrl(url))) {
  74. return
  75. }
  76. if (options.base) {
  77. const base = options.base
  78. const set: string[] = []
  79. let needImportTransform = false
  80. imageCandidates.forEach(candidate => {
  81. let { url, descriptor } = candidate
  82. descriptor = descriptor ? ` ${descriptor}` : ``
  83. if (url[0] === '.') {
  84. candidate.url = (path.posix || path).join(base, url)
  85. set.push(candidate.url + descriptor)
  86. } else if (shouldProcessUrl(url)) {
  87. needImportTransform = true
  88. } else {
  89. set.push(url + descriptor)
  90. }
  91. })
  92. if (!needImportTransform) {
  93. attr.value.content = set.join(', ')
  94. return
  95. }
  96. }
  97. const compoundExpression = createCompoundExpression([], attr.loc)
  98. imageCandidates.forEach(({ url, descriptor }, index) => {
  99. if (shouldProcessUrl(url)) {
  100. const { path } = parseUrl(url)
  101. let exp: SimpleExpressionNode
  102. if (path) {
  103. const existingImportsIndex = context.imports.findIndex(
  104. i => i.path === path
  105. )
  106. if (existingImportsIndex > -1) {
  107. exp = createSimpleExpression(
  108. `_imports_${existingImportsIndex}`,
  109. false,
  110. attr.loc,
  111. ConstantTypes.CAN_STRINGIFY
  112. )
  113. } else {
  114. exp = createSimpleExpression(
  115. `_imports_${context.imports.length}`,
  116. false,
  117. attr.loc,
  118. ConstantTypes.CAN_STRINGIFY
  119. )
  120. context.imports.push({ exp, path })
  121. }
  122. compoundExpression.children.push(exp)
  123. }
  124. } else {
  125. const exp = createSimpleExpression(
  126. `"${url}"`,
  127. false,
  128. attr.loc,
  129. ConstantTypes.CAN_STRINGIFY
  130. )
  131. compoundExpression.children.push(exp)
  132. }
  133. const isNotLast = imageCandidates.length - 1 > index
  134. if (descriptor && isNotLast) {
  135. compoundExpression.children.push(` + ' ${descriptor}, ' + `)
  136. } else if (descriptor) {
  137. compoundExpression.children.push(` + ' ${descriptor}'`)
  138. } else if (isNotLast) {
  139. compoundExpression.children.push(` + ', ' + `)
  140. }
  141. })
  142. let exp: ExpressionNode = compoundExpression
  143. if (context.hoistStatic) {
  144. exp = context.hoist(compoundExpression)
  145. exp.constType = ConstantTypes.CAN_STRINGIFY
  146. }
  147. node.props[index] = {
  148. type: NodeTypes.DIRECTIVE,
  149. name: 'bind',
  150. arg: createSimpleExpression('srcset', true, attr.loc),
  151. exp,
  152. modifiers: [],
  153. loc: attr.loc
  154. }
  155. }
  156. })
  157. }
  158. }
  159. }