|  | 
|  | 1 | +// Inlined version of code from Vite <https://github.com/vitejs/vite> | 
|  | 2 | +// Copyright (c) 2019-present, VoidZero Inc. and Vite contributors | 
|  | 3 | +// Released under the MIT License. | 
|  | 4 | +// | 
|  | 5 | +// Minor modifications have been made to work with the Tailwind CSS codebase | 
|  | 6 | + | 
|  | 7 | +import * as path from 'node:path' | 
|  | 8 | +import { toCss, walk } from '../../tailwindcss/src/ast' | 
|  | 9 | +import { parse } from '../../tailwindcss/src/css-parser' | 
|  | 10 | +import { normalizePath } from './normalize-path' | 
|  | 11 | + | 
|  | 12 | +const cssUrlRE = | 
|  | 13 | +  /(?<!@import\s+)(?<=^|[^\w\-\u0080-\uffff])url\((\s*('[^']+'|"[^"]+")\s*|[^'")]+)\)/ | 
|  | 14 | +const cssImageSetRE = /(?<=image-set\()((?:[\w-]{1,256}\([^)]*\)|[^)])*)(?=\))/ | 
|  | 15 | +const cssNotProcessedRE = /(?:gradient|element|cross-fade|image)\(/ | 
|  | 16 | + | 
|  | 17 | +const dataUrlRE = /^\s*data:/i | 
|  | 18 | +const externalRE = /^([a-z]+:)?\/\// | 
|  | 19 | +const functionCallRE = /^[A-Z_][.\w-]*\(/i | 
|  | 20 | + | 
|  | 21 | +const imageCandidateRE = | 
|  | 22 | +  /(?:^|\s)(?<url>[\w-]+\([^)]*\)|"[^"]*"|'[^']*'|[^,]\S*[^,])\s*(?:\s(?<descriptor>\w[^,]+))?(?:,|$)/g | 
|  | 23 | +const nonEscapedDoubleQuoteRE = /(?<!\\)"/g | 
|  | 24 | +const escapedSpaceCharactersRE = /(?: |\\t|\\n|\\f|\\r)+/g | 
|  | 25 | + | 
|  | 26 | +const isDataUrl = (url: string): boolean => dataUrlRE.test(url) | 
|  | 27 | +const isExternalUrl = (url: string): boolean => externalRE.test(url) | 
|  | 28 | + | 
|  | 29 | +type CssUrlReplacer = (url: string, importer?: string) => string | Promise<string> | 
|  | 30 | + | 
|  | 31 | +interface ImageCandidate { | 
|  | 32 | +  url: string | 
|  | 33 | +  descriptor: string | 
|  | 34 | +} | 
|  | 35 | + | 
|  | 36 | +export async function rewriteUrls({ | 
|  | 37 | +  css, | 
|  | 38 | +  base, | 
|  | 39 | +  root, | 
|  | 40 | +}: { | 
|  | 41 | +  css: string | 
|  | 42 | +  base: string | 
|  | 43 | +  root: string | 
|  | 44 | +}) { | 
|  | 45 | +  if (!css.includes('url(') && !css.includes('image-set(')) { | 
|  | 46 | +    return css | 
|  | 47 | +  } | 
|  | 48 | + | 
|  | 49 | +  let ast = parse(css) | 
|  | 50 | + | 
|  | 51 | +  let promises: Promise<void>[] = [] | 
|  | 52 | + | 
|  | 53 | +  function replacerForDeclaration(url: string) { | 
|  | 54 | +    let absoluteUrl = path.posix.join(normalizePath(base), url) | 
|  | 55 | +    let relativeUrl = path.posix.relative(normalizePath(root), absoluteUrl) | 
|  | 56 | + | 
|  | 57 | +    // If the path points to a file in the same directory, `path.relative` will | 
|  | 58 | +    // remove the leading `./` and we need to add it back in order to still | 
|  | 59 | +    // consider the path relative | 
|  | 60 | +    if (!relativeUrl.startsWith('.')) { | 
|  | 61 | +      relativeUrl = './' + relativeUrl | 
|  | 62 | +    } | 
|  | 63 | + | 
|  | 64 | +    return relativeUrl | 
|  | 65 | +  } | 
|  | 66 | + | 
|  | 67 | +  walk(ast, (node) => { | 
|  | 68 | +    if (node.kind !== 'declaration') return | 
|  | 69 | +    if (!node.value) return | 
|  | 70 | + | 
|  | 71 | +    let isCssUrl = cssUrlRE.test(node.value) | 
|  | 72 | +    let isCssImageSet = cssImageSetRE.test(node.value) | 
|  | 73 | + | 
|  | 74 | +    if (isCssUrl || isCssImageSet) { | 
|  | 75 | +      let rewriterToUse = isCssImageSet ? rewriteCssImageSet : rewriteCssUrls | 
|  | 76 | + | 
|  | 77 | +      promises.push( | 
|  | 78 | +        rewriterToUse(node.value, replacerForDeclaration).then((url) => { | 
|  | 79 | +          node.value = url | 
|  | 80 | +        }), | 
|  | 81 | +      ) | 
|  | 82 | +    } | 
|  | 83 | +  }) | 
|  | 84 | + | 
|  | 85 | +  if (promises.length) { | 
|  | 86 | +    await Promise.all(promises) | 
|  | 87 | +  } | 
|  | 88 | + | 
|  | 89 | +  return toCss(ast, { | 
|  | 90 | +    printUtilitiesNode: true, | 
|  | 91 | +  }) | 
|  | 92 | +} | 
|  | 93 | + | 
|  | 94 | +function rewriteCssUrls(css: string, replacer: CssUrlReplacer): Promise<string> { | 
|  | 95 | +  return asyncReplace(css, cssUrlRE, async (match) => { | 
|  | 96 | +    const [matched, rawUrl] = match | 
|  | 97 | +    return await doUrlReplace(rawUrl.trim(), matched, replacer) | 
|  | 98 | +  }) | 
|  | 99 | +} | 
|  | 100 | + | 
|  | 101 | +async function rewriteCssImageSet(css: string, replacer: CssUrlReplacer): Promise<string> { | 
|  | 102 | +  return await asyncReplace(css, cssImageSetRE, async (match) => { | 
|  | 103 | +    const [, rawUrl] = match | 
|  | 104 | +    const url = await processSrcSet(rawUrl, async ({ url }) => { | 
|  | 105 | +      // the url maybe url(...) | 
|  | 106 | +      if (cssUrlRE.test(url)) { | 
|  | 107 | +        return await rewriteCssUrls(url, replacer) | 
|  | 108 | +      } | 
|  | 109 | +      if (!cssNotProcessedRE.test(url)) { | 
|  | 110 | +        return await doUrlReplace(url, url, replacer) | 
|  | 111 | +      } | 
|  | 112 | +      return url | 
|  | 113 | +    }) | 
|  | 114 | +    return url | 
|  | 115 | +  }) | 
|  | 116 | +} | 
|  | 117 | + | 
|  | 118 | +async function doUrlReplace( | 
|  | 119 | +  rawUrl: string, | 
|  | 120 | +  matched: string, | 
|  | 121 | +  replacer: CssUrlReplacer, | 
|  | 122 | +  funcName: string = 'url', | 
|  | 123 | +) { | 
|  | 124 | +  let wrap = '' | 
|  | 125 | +  const first = rawUrl[0] | 
|  | 126 | +  if (first === `"` || first === `'`) { | 
|  | 127 | +    wrap = first | 
|  | 128 | +    rawUrl = rawUrl.slice(1, -1) | 
|  | 129 | +  } | 
|  | 130 | + | 
|  | 131 | +  if (skipUrlReplacer(rawUrl)) { | 
|  | 132 | +    return matched | 
|  | 133 | +  } | 
|  | 134 | + | 
|  | 135 | +  let newUrl = await replacer(rawUrl) | 
|  | 136 | +  // The new url might need wrapping even if the original did not have it, e.g. if a space was added during replacement | 
|  | 137 | +  if (wrap === '' && newUrl !== encodeURI(newUrl)) { | 
|  | 138 | +    wrap = '"' | 
|  | 139 | +  } | 
|  | 140 | +  // If wrapping in single quotes and newUrl also contains single quotes, switch to double quotes. | 
|  | 141 | +  // Give preference to double quotes since SVG inlining converts double quotes to single quotes. | 
|  | 142 | +  if (wrap === "'" && newUrl.includes("'")) { | 
|  | 143 | +    wrap = '"' | 
|  | 144 | +  } | 
|  | 145 | +  // Escape double quotes if they exist (they also tend to be rarer than single quotes) | 
|  | 146 | +  if (wrap === '"' && newUrl.includes('"')) { | 
|  | 147 | +    newUrl = newUrl.replace(nonEscapedDoubleQuoteRE, '\\"') | 
|  | 148 | +  } | 
|  | 149 | +  return `${funcName}(${wrap}${newUrl}${wrap})` | 
|  | 150 | +} | 
|  | 151 | + | 
|  | 152 | +function skipUrlReplacer(rawUrl: string) { | 
|  | 153 | +  return ( | 
|  | 154 | +    isExternalUrl(rawUrl) || isDataUrl(rawUrl) || rawUrl[0] === '#' || functionCallRE.test(rawUrl) | 
|  | 155 | +  ) | 
|  | 156 | +} | 
|  | 157 | + | 
|  | 158 | +function processSrcSet( | 
|  | 159 | +  srcs: string, | 
|  | 160 | +  replacer: (arg: ImageCandidate) => Promise<string>, | 
|  | 161 | +): Promise<string> { | 
|  | 162 | +  return Promise.all( | 
|  | 163 | +    parseSrcset(srcs).map(async ({ url, descriptor }) => ({ | 
|  | 164 | +      url: await replacer({ url, descriptor }), | 
|  | 165 | +      descriptor, | 
|  | 166 | +    })), | 
|  | 167 | +  ).then(joinSrcset) | 
|  | 168 | +} | 
|  | 169 | + | 
|  | 170 | +function parseSrcset(string: string): ImageCandidate[] { | 
|  | 171 | +  const matches = string | 
|  | 172 | +    .trim() | 
|  | 173 | +    .replace(escapedSpaceCharactersRE, ' ') | 
|  | 174 | +    .replace(/\r?\n/, '') | 
|  | 175 | +    .replace(/,\s+/, ', ') | 
|  | 176 | +    .replaceAll(/\s+/g, ' ') | 
|  | 177 | +    .matchAll(imageCandidateRE) | 
|  | 178 | +  return Array.from(matches, ({ groups }) => ({ | 
|  | 179 | +    url: groups?.url?.trim() ?? '', | 
|  | 180 | +    descriptor: groups?.descriptor?.trim() ?? '', | 
|  | 181 | +  })).filter(({ url }) => !!url) | 
|  | 182 | +} | 
|  | 183 | + | 
|  | 184 | +function joinSrcset(ret: ImageCandidate[]) { | 
|  | 185 | +  return ret.map(({ url, descriptor }) => url + (descriptor ? ` ${descriptor}` : '')).join(', ') | 
|  | 186 | +} | 
|  | 187 | + | 
|  | 188 | +async function asyncReplace( | 
|  | 189 | +  input: string, | 
|  | 190 | +  re: RegExp, | 
|  | 191 | +  replacer: (match: RegExpExecArray) => string | Promise<string>, | 
|  | 192 | +): Promise<string> { | 
|  | 193 | +  let match: RegExpExecArray | null | 
|  | 194 | +  let remaining = input | 
|  | 195 | +  let rewritten = '' | 
|  | 196 | +  while ((match = re.exec(remaining))) { | 
|  | 197 | +    rewritten += remaining.slice(0, match.index) | 
|  | 198 | +    rewritten += await replacer(match) | 
|  | 199 | +    remaining = remaining.slice(match.index + match[0].length) | 
|  | 200 | +  } | 
|  | 201 | +  rewritten += remaining | 
|  | 202 | +  return rewritten | 
|  | 203 | +} | 
0 commit comments