expansive.es 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. /*
  2. expansive.es - Configuration for exp-css
  3. Transform by prefixing and minifying. Uses autoprefixer and less.
  4. */
  5. Expansive.load({
  6. services: {
  7. name: 'css',
  8. dotmin: true,
  9. files: [ '**.css*', '!**.map', '!*.less*' ],
  10. force: false,
  11. extract: false,
  12. minify: false,
  13. prefix: true,
  14. usemap: true,
  15. usemin: true,
  16. transforms: [{
  17. mappings: {
  18. 'css',
  19. 'min.css'
  20. 'css.map'
  21. },
  22. init: function(transform) {
  23. let service = transform.service
  24. if (service.minify) {
  25. service.usemin ||= true
  26. }
  27. if (service.usemap) {
  28. service.usemin ||= true
  29. }
  30. if (service.files) {
  31. if (!(service.files is Array)) {
  32. service.files = [ service.files ]
  33. }
  34. expansive.control.collections.styles =
  35. (expansive.control.collections.styles + service.files).unique()
  36. }
  37. if (!service.minify) {
  38. expansive.transforms['css-minify'].enable = false
  39. }
  40. if (!service.extract) {
  41. expansive.transforms['css-extract'].enable = false
  42. }
  43. }
  44. resolve: function(path: Path, transform): Path? {
  45. let service = transform.service
  46. let vfile = directories.contents.join(path)
  47. if (vfile.endsWith('.min.css')) {
  48. let base = vfile.trimEnd('.min.css')
  49. if (service.usemin && !(service.minify && service.force && base.joinExt('css', true).exists)) {
  50. if (service.usemap) {
  51. if (base.joinExt('css.map', true).exists) {
  52. return terminal(path)
  53. }
  54. } else {
  55. return terminal(path)
  56. }
  57. }
  58. } else if (vfile.endsWith('.css.map')) {
  59. if (service.usemin) {
  60. let base = vfile.trimEnd('.css.map')
  61. if (service.usemap && !(service.minify && service.force && base.joinExt('css', true).exists)) {
  62. if (base.joinExt('min.css', true).exists) {
  63. return terminal(path)
  64. }
  65. }
  66. }
  67. } else {
  68. let minified = vfile.replaceExt('min.css')
  69. /*
  70. Use this source file if forced+miify, or a suitable minfied version does not exist or !usemin
  71. */
  72. if ((service.minify && service.force) ||
  73. !(minified.exists && service.usemin && (!service.usemap || vfile.replaceExt('css.map').exists))) {
  74. if (service.minify && service.dotmin) {
  75. return terminal(path.trimExt().joinExt('min.css', true))
  76. }
  77. return terminal(path)
  78. }
  79. }
  80. return null
  81. }
  82. }, {
  83. name: 'prefix',
  84. mappings: 'css',
  85. render: function(contents) {
  86. let autoprefixer = Cmd.locate('autoprefixer')
  87. if (autoprefixer) {
  88. contents = expansive.run(autoprefixer, contents)
  89. } else {
  90. trace('Warn', 'Cannot find autoprefixer')
  91. }
  92. return contents
  93. }
  94. }, {
  95. name: 'minify',
  96. mappings: 'css',
  97. render: function(contents, meta) {
  98. trace('Minify', meta.current)
  99. let less = Cmd.locate('lessc')
  100. if (less) {
  101. contents = expansive.run(less + ' --compress - ', contents, meta)
  102. } else {
  103. trace('Warn', 'Cannot find lessc')
  104. }
  105. return contents
  106. }
  107. }, {
  108. name: 'render',
  109. mappings: {
  110. 'css',
  111. 'min.css'
  112. },
  113. init: function(transform) {
  114. let service = transform.service
  115. service.hash = {}
  116. /*
  117. Render styles is based on 'collections.styles' which defaults to '**.css' and is modified
  118. via expansive.json and addItems.
  119. */
  120. global.renderStyles = function(filter = null, extras = []) {
  121. let collections = expansive.collections
  122. if (!collections.styles) {
  123. return
  124. }
  125. function buildStyleList(files: Array): Array {
  126. let directories = expansive.directories
  127. let service = expansive.services.css
  128. let styles = []
  129. for each (style in files) {
  130. if ((style = expansive.getDestPath(style)) == null) {
  131. continue
  132. }
  133. let vfile = directories.contents.join(style)
  134. let base = vfile.trimEnd('.min.css').trimEnd('.css')
  135. let map = base.joinExt('min.map', true).exists || base.joinExt('css.map', true).exists ||
  136. base.joinExt('.min.css.map', true).exists
  137. if (vfile.endsWith('min.css')) {
  138. if (service.usemin && (!service.usemap || map)) {
  139. styles.push(style)
  140. }
  141. } else {
  142. let minified = vfile.replaceExt('min.css').exists
  143. let map = vfile.replaceExt('min.map').exists || vfile.replaceExt('css.map').exists ||
  144. vfile.replaceExt('.min.css.map').exists
  145. if ((service.minify && service.force) || !minified || !(service.usemap && map)) {
  146. if (service.minify && service.dotmin) {
  147. styles.push(style.replaceExt('min.css'))
  148. } else {
  149. styles.push(style)
  150. }
  151. }
  152. }
  153. }
  154. return styles
  155. }
  156. let directories = expansive.directories
  157. let service = expansive.services.css
  158. /*
  159. Pages have different stylesheets and so must compute style list per page.
  160. This is hased and saved.
  161. */
  162. if (!service.hash[collections.styles]) {
  163. let files = directories.contents.files(collections.styles,
  164. { contents: true, directories: false, relative: true})
  165. files = expansive.orderFiles(files, "css")
  166. service.hash[collections.styles] = buildStyleList(files).unique()
  167. }
  168. for each (style in service.hash[collections.styles]) {
  169. if (filter && !Path(script).glob(filter)) {
  170. continue
  171. }
  172. let uri = meta.top.join(style).trimStart('./')
  173. write('<link href="' + uri + '" rel="stylesheet" type="text/css" />\n ')
  174. }
  175. if (extras && extras is String) {
  176. extras = [extras]
  177. }
  178. if (service.states) {
  179. let extracted = service.states[meta.destPath]
  180. if (extracted && extracted.link) {
  181. let ct = expansive.transforms.css
  182. extras.push(ct.resolve(extracted.link, ct))
  183. }
  184. }
  185. for each (style in extras) {
  186. let uri = meta.top.join(style).trimStart('./')
  187. write('<link href="' + uri + '" rel="stylesheet" type="text/css" />\n ')
  188. }
  189. if (expansive.collections['inline-styles']) {
  190. write('<style>')
  191. for each (style in expansive.collections['inline-styles']) {
  192. write(style)
  193. }
  194. write('\n </style>')
  195. }
  196. }
  197. },
  198. pre: function (transform) {
  199. if (expansive.modified.everything) {
  200. transform.service.hash = {}
  201. }
  202. },
  203. }, {
  204. name: 'extract',
  205. mappings: 'html',
  206. init: function(transform) {
  207. let service = transform
  208. if (!service.extract) {
  209. transform.enable = false
  210. return
  211. }
  212. transform.nextId = 0
  213. service.states = {}
  214. },
  215. render: function(contents, meta, transform) {
  216. /*
  217. Local function to handle inline style elements
  218. */
  219. function handleStyleElements(contents, meta, state, transform): String {
  220. let re = /<style[^>]*>(.*)<\/style>/smg
  221. let start = 0, end = 0
  222. let output = ''
  223. let matches
  224. while (matches = re.exec(contents, start)) {
  225. let elt = matches[0]
  226. end = re.lastIndex - elt.length
  227. output += contents.slice(start, end)
  228. state.elements ||= []
  229. state.elements.push(matches[1].trimEnd(';'))
  230. start = re.lastIndex
  231. }
  232. output += contents.slice(start)
  233. return output
  234. }
  235. /*
  236. Local function to handle style attributes
  237. */
  238. function handleStyleAttributes(contents, meta, state, transform): String {
  239. let result = ''
  240. let re = /<([\w_\-:.]+)([^>]*>)/g
  241. let start = 0, end = 0
  242. let matches
  243. while (matches = re.exec(contents, start)) {
  244. let elt = matches[0]
  245. end = re.lastIndex - matches[0].length
  246. /* Emit prior contents */
  247. result += contents.slice(start, end)
  248. let attre = /(.*) +(style=)(["'])(.*?)(\3)(.*)/m
  249. if ((cmatches = attre.exec(elt)) != null) {
  250. elt = cmatches[1] + cmatches[6]
  251. let id
  252. if ((ematches = elt.match(/id=['"]([^'"]+)['"]/)) != null) {
  253. id = '#' + ematches[1]
  254. } else {
  255. id = 'exp-' + ++transform.nextId
  256. }
  257. state.attributes ||= {}
  258. state.attributes[id] = cmatches[4].trimEnd(';')
  259. if (!id.startsWith('#')) {
  260. let classre = /(.*) +(class=)(["'])([^'"]+)(\3)(.*)/m
  261. if ((classes = classre.exec(elt)) != null) {
  262. elt = classes[1] + ' ' + classes[2] + '"' + classes[4] + ' ' + id + '"' + classes[6]
  263. } else {
  264. elt = cmatches[1] + ' class="' + id + '" ' + cmatches[6]
  265. }
  266. } else {
  267. elt = cmatches[1] + cmatches[6]
  268. }
  269. }
  270. result += elt
  271. start = re.lastIndex
  272. }
  273. return result + contents.slice(start)
  274. }
  275. let service = transform.service
  276. let state = {}
  277. contents = handleStyleElements(contents, meta, state, transform)
  278. contents = handleStyleAttributes(contents, meta, state, transform)
  279. if (state.elements || state.attributes) {
  280. let ss = service.states[meta.destPath] ||= {}
  281. if (service.extract === true) {
  282. ss.link = meta.destPath.replaceExt('css')
  283. } else {
  284. ss.link = service.extract
  285. }
  286. if (state.elements) {
  287. ss.elements ||= []
  288. ss.elements += state.elements
  289. vtrace('Extract', 'Scripts from ' + meta.document)
  290. }
  291. if (state.attributes) {
  292. ss.attributes ||= {}
  293. blend(ss.attributes, state.attributes)
  294. vtrace('Extract', 'Onclick from ' + meta.document)
  295. }
  296. }
  297. return contents
  298. },
  299. /*
  300. Post process and create external stylesheets for inline styles
  301. */
  302. post: function(transform) {
  303. let service = transform.service
  304. let perdoc = (service.extract !== true)
  305. let styles = '/*\n Inline styles\n */\n'
  306. for (let [file, state] in service.states) {
  307. if (perdoc) {
  308. styles = '/*\n Inline styles for ' + file + '\n */\n'
  309. }
  310. if (state.elements) {
  311. for (let [key,value] in state.elements) {
  312. value = value.trim().split('\n').transform(function (e) e.trim()).join('\n ')
  313. // Match {
  314. value = value.replace(' }', '}')
  315. state.elements[key] = value
  316. }
  317. styles += state.elements.join('\n\n') + '\n\n'
  318. }
  319. for (let [id, code] in state.attributes) {
  320. styles += id + ' {\n ' + code + '\n}\n\n'
  321. }
  322. if (perdoc) {
  323. let destPath = Path(file).replaceExt('css')
  324. let dest = directories.dist.join(destPath)
  325. let meta = blend(expansive.topMeta.clone(), { document: destPath, layout: null })
  326. styles = renderContents(styles, meta)
  327. writeDest(styles, meta)
  328. }
  329. }
  330. if (!perdoc) {
  331. let destPath = service.extract
  332. let dest = directories.dist.join(destPath)
  333. let meta = blend(expansive.topMeta.clone(), { document: destPath, layout: null })
  334. styles = renderContents(styles, meta)
  335. writeDest(styles, meta)
  336. }
  337. }
  338. }]
  339. }
  340. })