expansive.es 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. /*
  2. expansive.es - Configuration for exp-css
  3. Transform by prefixing and minifying. Uses postcss 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. if (expansive.control.collections.styles.length == 0) {
  35. expansive.control.collections.styles = service.files
  36. }
  37. }
  38. if (!service.minify) {
  39. expansive.transforms['css-minify'].enable = false
  40. }
  41. if (!service.extract) {
  42. expansive.transforms['css-extract'].enable = false
  43. }
  44. }
  45. resolve: function(path: Path, transform): Path? {
  46. let service = transform.service
  47. let vfile = directories.contents.join(path)
  48. if (vfile.endsWith('.min.css')) {
  49. let base = vfile.trimEnd('.min.css')
  50. if (service.usemin && !(service.minify && service.force && base.joinExt('css', true).exists)) {
  51. if (service.usemap) {
  52. if (base.joinExt('css.map', true).exists) {
  53. return terminal(path)
  54. }
  55. } else {
  56. return terminal(path)
  57. }
  58. }
  59. } else if (vfile.endsWith('.css.map')) {
  60. if (service.usemin) {
  61. let base = vfile.trimEnd('.css.map')
  62. if (service.usemap && !(service.minify && service.force && base.joinExt('css', true).exists)) {
  63. if (base.joinExt('min.css', true).exists) {
  64. return terminal(path)
  65. }
  66. }
  67. }
  68. } else {
  69. let minified = vfile.replaceExt('min.css')
  70. /*
  71. Use this source file if forced+miify, or a suitable minfied version does not exist or !usemin
  72. */
  73. if ((service.minify && service.force) ||
  74. !(minified.exists && service.usemin && (!service.usemap || vfile.replaceExt('css.map').exists))) {
  75. if (service.minify && service.dotmin) {
  76. return terminal(path.trimExt().joinExt('min.css', true))
  77. }
  78. return terminal(path)
  79. }
  80. }
  81. return null
  82. }
  83. }, {
  84. name: 'prefix',
  85. mappings: 'css',
  86. render: function(contents) {
  87. let postcss = Cmd.locate('postcss')
  88. if (postcss) {
  89. contents = expansive.run(postcss + ' --use autoprefixer', contents)
  90. } else {
  91. trace('Warn', 'Cannot find postcss')
  92. }
  93. return contents
  94. }
  95. }, {
  96. name: 'minify',
  97. mappings: 'css',
  98. render: function(contents, meta) {
  99. trace('Minify', meta.current)
  100. let less = Cmd.locate('lessc')
  101. if (less) {
  102. contents = expansive.run(less + ' --compress - ', contents, meta)
  103. } else {
  104. trace('Warn', 'Cannot find lessc')
  105. }
  106. return contents
  107. }
  108. }, {
  109. name: 'render',
  110. mappings: {
  111. 'css',
  112. 'min.css'
  113. },
  114. init: function(transform) {
  115. let service = transform.service
  116. service.hash = {}
  117. /*
  118. Render styles is based on 'collections.styles' which defaults to '**.css' and is modified 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. if (expansive.control.filters) {
  166. files = files.filter(function(e) { return e.glob(expansive.control.filters) })
  167. }
  168. files = expansive.orderFiles(files, "css")
  169. service.hash[collections.styles] = buildStyleList(files).unique()
  170. }
  171. for each (style in service.hash[collections.styles]) {
  172. if (filter && !Path(style).glob(filter)) {
  173. continue
  174. }
  175. if (service.absolute) {
  176. if (!style.startsWith('http') && !style.startsWith('..')) {
  177. style = '/' + style
  178. }
  179. } else {
  180. style = meta.top.join(style).trimStart('./')
  181. }
  182. write('<link href="' + style + '" rel="stylesheet" type="text/css" />\n ')
  183. }
  184. if (extras && extras is String) {
  185. extras = [extras]
  186. }
  187. if (service.states) {
  188. let extracted = service.states[meta.destPath]
  189. if (extracted && extracted.link) {
  190. let ct = expansive.transforms.css
  191. extras.push(ct.resolve(extracted.link, ct))
  192. }
  193. }
  194. for each (style in extras) {
  195. if (service.absolute) {
  196. if (!style.startsWith('http') && !style.startsWith('..')) {
  197. style = '/' + style
  198. }
  199. } else {
  200. style = meta.top.join(style).trimStart('./')
  201. }
  202. write('<link href="' + style + '" rel="stylesheet" type="text/css" />\n ')
  203. }
  204. if (expansive.collections['inline-styles']) {
  205. write('<style>')
  206. for each (style in expansive.collections['inline-styles']) {
  207. write(style)
  208. }
  209. write('\n </style>')
  210. }
  211. }
  212. },
  213. pre: function (transform) {
  214. if (expansive.modified.everything) {
  215. transform.service.hash = {}
  216. }
  217. },
  218. }, {
  219. name: 'extract',
  220. mappings: 'html',
  221. init: function(transform) {
  222. let service = transform
  223. if (!service.extract) {
  224. transform.enable = false
  225. return
  226. }
  227. transform.nextId = 0
  228. service.states = {}
  229. },
  230. render: function(contents, meta, transform) {
  231. /*
  232. Local function to handle inline style elements
  233. */
  234. function handleStyleElements(contents, meta, state, transform): String {
  235. let re = /<style[^>]*>(.*)<\/style>/smg
  236. let start = 0, end = 0
  237. let output = ''
  238. let matches
  239. while (matches = re.exec(contents, start)) {
  240. let elt = matches[0]
  241. end = re.lastIndex - elt.length
  242. output += contents.slice(start, end)
  243. state.elements ||= []
  244. state.elements.push(matches[1].trimEnd(';'))
  245. start = re.lastIndex
  246. }
  247. output += contents.slice(start)
  248. return output
  249. }
  250. /*
  251. Local function to handle style attributes
  252. */
  253. function handleStyleAttributes(contents, meta, state, transform): String {
  254. let result = ''
  255. let re = /<([\w_\-:.]+)([^>]*>)/g
  256. let start = 0, end = 0
  257. let matches
  258. while (matches = re.exec(contents, start)) {
  259. let elt = matches[0]
  260. end = re.lastIndex - matches[0].length
  261. /* Emit prior contents */
  262. result += contents.slice(start, end)
  263. let attre = /(.*) +(style=)(["'])(.*?)(\3)(.*)/m
  264. if ((cmatches = attre.exec(elt)) != null) {
  265. elt = cmatches[1] + cmatches[6]
  266. let id
  267. if ((ematches = elt.match(/id=['"]([^'"]+)['"]/)) != null) {
  268. id = '#' + ematches[1]
  269. } else {
  270. id = 'exp-' + ++transform.nextId
  271. }
  272. state.attributes ||= {}
  273. state.attributes[id] = cmatches[4].trimEnd(';')
  274. if (!id.startsWith('#')) {
  275. let classre = /(.*) +(class=)(["'])([^'"]+)(\3)(.*)/m
  276. if ((classes = classre.exec(elt)) != null) {
  277. elt = classes[1] + ' ' + classes[2] + '"' + classes[4] + ' ' + id + '"' + classes[6]
  278. } else {
  279. elt = cmatches[1] + ' class="' + id + '" ' + cmatches[6]
  280. }
  281. } else {
  282. elt = cmatches[1] + cmatches[6]
  283. }
  284. }
  285. result += elt
  286. start = re.lastIndex
  287. }
  288. return result + contents.slice(start)
  289. }
  290. let service = transform.service
  291. let state = {}
  292. contents = handleStyleElements(contents, meta, state, transform)
  293. contents = handleStyleAttributes(contents, meta, state, transform)
  294. if (state.elements || state.attributes) {
  295. let ss = service.states[meta.destPath] ||= {}
  296. if (service.extract === true) {
  297. ss.link = meta.destPath.replaceExt('css')
  298. } else {
  299. ss.link = service.extract
  300. }
  301. if (state.elements) {
  302. ss.elements ||= []
  303. ss.elements += state.elements
  304. vtrace('Extract', 'Scripts from ' + meta.document)
  305. }
  306. if (state.attributes) {
  307. ss.attributes ||= {}
  308. blend(ss.attributes, state.attributes)
  309. vtrace('Extract', 'Onclick from ' + meta.document)
  310. }
  311. }
  312. return contents
  313. },
  314. /*
  315. Post process and create external stylesheets for inline styles
  316. */
  317. post: function(transform) {
  318. let service = transform.service
  319. let perdoc = (service.extract !== true)
  320. let styles = '/*\n Inline styles\n */\n'
  321. for (let [file, state] in service.states) {
  322. if (perdoc) {
  323. styles = '/*\n Inline styles for ' + file + '\n */\n'
  324. }
  325. if (state.elements) {
  326. for (let [key,value] in state.elements) {
  327. value = value.trim().split('\n').transform(function (e) e.trim()).join('\n ')
  328. // Match {{
  329. value = value.replace(' }', '}')
  330. state.elements[key] = value
  331. }
  332. styles += state.elements.join('\n\n') + '\n\n'
  333. }
  334. for (let [id, code] in state.attributes) {
  335. styles += id + ' {\n ' + code + '\n}\n\n'
  336. }
  337. if (perdoc) {
  338. let destPath = Path(file).replaceExt('css')
  339. let dest = directories.dist.join(destPath)
  340. let meta = blend(expansive.topMeta.clone(), { document: destPath, layout: null })
  341. styles = renderContents(styles, meta)
  342. writeDest(styles, meta)
  343. }
  344. }
  345. if (!perdoc) {
  346. let destPath = service.extract
  347. let dest = directories.dist.join(destPath)
  348. let meta = blend(expansive.topMeta.clone(), { document: destPath, layout: null })
  349. styles = renderContents(styles, meta)
  350. writeDest(styles, meta)
  351. }
  352. }
  353. }]
  354. }
  355. })