templateTransformSrcset.ts 5.0 KB

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