plugin.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. 'use strict'
  2. var config = require('./config.js')
  3. var util = require('./util.js')
  4. module.exports = {
  5. 'name': 'rtlcss',
  6. 'priority': 100,
  7. 'directives': {
  8. 'control': {
  9. 'ignore': {
  10. 'expect': { 'atrule': true, 'comment': true, 'decl': true, 'rule': true },
  11. 'endNode': null,
  12. 'begin': function (node, metadata, context) {
  13. // find the ending node in case of self closing directive
  14. if (!this.endNode && metadata.begin && metadata.end) {
  15. var n = node
  16. while (n && n.nodes) {
  17. n = n.nodes[n.nodes.length - 1]
  18. }
  19. this.endNode = n
  20. }
  21. var prevent = true
  22. if (node.type === 'comment' && node.text.match(/^\s*!?\s*rtl:end:ignore/)) {
  23. prevent = false
  24. }
  25. return prevent
  26. },
  27. 'end': function (node, metadata, context) {
  28. // end if:
  29. // 1. block directive and the node is comment
  30. // 2. self closing directive and node is endNode
  31. if (metadata.begin !== metadata.end && node.type === 'comment' || metadata.begin && metadata.end && node === this.endNode) {
  32. // clear ending node
  33. this.endNode = null
  34. return true
  35. }
  36. return false
  37. }
  38. },
  39. 'rename': {
  40. 'expect': {'rule': true},
  41. 'begin': function (node, metadata, context) {
  42. node.selector = context.util.applyStringMap(node.selector, false)
  43. return false
  44. },
  45. 'end': function (node, context) {
  46. return true
  47. }
  48. },
  49. 'raw': {
  50. 'expect': {'self': true},
  51. 'begin': function (node, metadata, context) {
  52. var nodes = context.postcss.parse(metadata.param)
  53. node.parent.insertBefore(node, nodes)
  54. return true
  55. },
  56. 'end': function (node, context) {
  57. return true
  58. }
  59. },
  60. 'remove': {
  61. 'expect': {'atrule': true, 'rule': true, 'decl': true},
  62. 'begin': function (node, metadata, context) {
  63. var prevent = false
  64. switch (node.type) {
  65. case 'atrule':
  66. case 'rule':
  67. case 'decl':
  68. prevent = true
  69. node.remove()
  70. }
  71. return prevent
  72. },
  73. 'end': function (node, metadata, context) {
  74. return true
  75. }
  76. },
  77. 'options': {
  78. 'expect': {'self': true},
  79. 'stack': [],
  80. 'begin': function (node, metadata, context) {
  81. this.stack.push(util.extend({}, context.config))
  82. var options
  83. try {
  84. options = JSON.parse(metadata.param)
  85. } catch (e) {
  86. throw node.error('Invlaid options object', { 'details': e })
  87. }
  88. context.config = config.configure(options, context.config.plugins)
  89. context.util = util.configure(context.config)
  90. return true
  91. },
  92. 'end': function (node, metadata, context) {
  93. var config = this.stack.pop()
  94. if (config && !metadata.begin) {
  95. context.config = config
  96. context.util = util.configure(context.config)
  97. }
  98. return true
  99. }
  100. },
  101. 'config': {
  102. 'expect': {'self': true},
  103. 'expr': {
  104. 'fn': /function([^\(]*)\(([^\(\)]*?)\)[^\{]*\{([^]*)\}/ig,
  105. 'rx': /\/([^\/]*)\/(.*)/ig
  106. },
  107. 'stack': [],
  108. 'begin': function (node, metadata, context) {
  109. this.stack.push(util.extend({}, context.config))
  110. var configuration
  111. try {
  112. configuration = eval('(' + metadata.param + ')') // eslint-disable-line no-eval
  113. } catch (e) {
  114. throw node.error('Invlaid config object', { 'details': e })
  115. }
  116. context.config = config.configure(configuration.options, configuration.plugins)
  117. context.util = util.configure(context.config)
  118. return true
  119. },
  120. 'end': function (node, metadata, context) {
  121. var config = this.stack.pop()
  122. if (config && !metadata.begin) {
  123. context.config = config
  124. context.util = util.configure(context.config)
  125. }
  126. return true
  127. }
  128. }
  129. },
  130. 'value': [
  131. {
  132. 'name': 'ignore',
  133. 'action': function (decl, expr, context) {
  134. return true
  135. }
  136. },
  137. {
  138. 'name': 'prepend',
  139. 'action': function (decl, expr, context) {
  140. var prefix = ''
  141. decl.raws.value.raw.replace(expr, function (m, v) {
  142. prefix += v
  143. })
  144. decl.value = decl.raws.value.raw = prefix + decl.raws.value.raw
  145. return true
  146. }
  147. },
  148. {
  149. 'name': 'append',
  150. 'action': function (decl, expr, context) {
  151. decl.value = decl.raws.value.raw = decl.raws.value.raw.replace(expr, function (match, value) {
  152. return match + value
  153. })
  154. return true
  155. }
  156. },
  157. {
  158. 'name': 'insert',
  159. 'action': function (decl, expr, context) {
  160. decl.value = decl.raws.value.raw = decl.raws.value.raw.replace(expr, function (match, value) {
  161. return value + match
  162. })
  163. return true
  164. }
  165. },
  166. {
  167. 'name': '',
  168. 'action': function (decl, expr, context) {
  169. decl.raws.value.raw.replace(expr, function (match, value) {
  170. decl.value = decl.raws.value.raw = value + match
  171. })
  172. return true
  173. }
  174. }
  175. ]
  176. },
  177. 'processors': [
  178. {
  179. 'name': 'variable',
  180. 'expr': /^--/im,
  181. 'action': function (prop, value) {
  182. return { 'prop': prop, 'value': value }
  183. }
  184. },
  185. {
  186. 'name': 'direction',
  187. 'expr': /direction/im,
  188. 'action': function (prop, value, context) {
  189. return { 'prop': prop, 'value': context.util.swapLtrRtl(value) }
  190. }
  191. },
  192. {
  193. 'name': 'left',
  194. 'expr': /left/im,
  195. 'action': function (prop, value, context) {
  196. return { 'prop': prop.replace(this.expr, function () { return 'right' }), 'value': value }
  197. }
  198. },
  199. {
  200. 'name': 'right',
  201. 'expr': /right/im,
  202. 'action': function (prop, value, context) {
  203. return { 'prop': prop.replace(this.expr, function () { return 'left' }), 'value': value }
  204. }
  205. },
  206. {
  207. 'name': 'four-value syntax',
  208. 'expr': /^(margin|padding|border-(color|style|width))$/ig,
  209. 'cache': null,
  210. 'action': function (prop, value, context) {
  211. if (this.cache === null) {
  212. this.cache = {
  213. 'match': /[^\s\uFFFD]+/g
  214. }
  215. }
  216. var state = context.util.guardFunctions(value)
  217. var result = state.value.match(this.cache.match)
  218. if (result && result.length === 4 && (state.store.length > 0 || result[1] !== result[3])) {
  219. var i = 0
  220. state.value = state.value.replace(this.cache.match, function () {
  221. return result[(4 - i++) % 4]
  222. })
  223. }
  224. return { 'prop': prop, 'value': context.util.unguardFunctions(state) }
  225. }
  226. },
  227. {
  228. 'name': 'border radius',
  229. 'expr': /border-radius/ig,
  230. 'cache': null,
  231. 'flip': function (value) {
  232. var parts = value.match(this.cache.match)
  233. var i
  234. if (parts) {
  235. switch (parts.length) {
  236. case 2:
  237. i = 1
  238. if (parts[0] !== parts[1]) {
  239. value = value.replace(this.cache.match, function () {
  240. return parts[i--]
  241. })
  242. }
  243. break
  244. case 3:
  245. // preserve leading whitespace.
  246. value = value.replace(this.cache.white, function (m) {
  247. return m + parts[1] + ' '
  248. })
  249. break
  250. case 4:
  251. i = 0
  252. if (parts[0] !== parts[1] || parts[2] !== parts[3]) {
  253. value = value.replace(this.cache.match, function () {
  254. return parts[(5 - i++) % 4]
  255. })
  256. }
  257. break
  258. }
  259. }
  260. return value
  261. },
  262. 'action': function (prop, value, context) {
  263. if (this.cache === null) {
  264. this.cache = {
  265. 'match': /[^\s\uFFFD]+/g,
  266. 'slash': /[^\/]+/g,
  267. 'white': /(^\s*)/
  268. }
  269. }
  270. var state = context.util.guardFunctions(value)
  271. state.value = state.value.replace(this.cache.slash, function (m) {
  272. return this.flip(m)
  273. }.bind(this))
  274. return { 'prop': prop, 'value': context.util.unguardFunctions(state) }
  275. }
  276. },
  277. {
  278. 'name': 'shadow',
  279. 'expr': /shadow/ig,
  280. 'cache': null,
  281. 'action': function (prop, value, context) {
  282. if (this.cache === null) {
  283. this.cache = {
  284. 'replace': /[^,]+/g
  285. }
  286. }
  287. var colorSafe = context.util.guardHexColors(value)
  288. var funcSafe = context.util.guardFunctions(colorSafe.value)
  289. funcSafe.value = funcSafe.value.replace(this.cache.replace, function (m) { return context.util.negate(m) })
  290. colorSafe.value = context.util.unguardFunctions(funcSafe)
  291. return { 'prop': prop, 'value': context.util.unguardHexColors(colorSafe) }
  292. }
  293. },
  294. {
  295. 'name': 'transform origin',
  296. 'expr': /transform-origin/ig,
  297. 'cache': null,
  298. 'flip': function (value, context) {
  299. if (value === '0') {
  300. value = '100%'
  301. } else if (value.match(this.cache.percent)) {
  302. value = context.util.complement(value)
  303. } else if (value.match(this.cache.length)) {
  304. value = context.util.flipLength(value)
  305. }
  306. return value
  307. },
  308. 'action': function (prop, value, context) {
  309. if (this.cache === null) {
  310. this.cache = {
  311. 'match': context.util.regex(['calc', 'percent', 'length'], 'g'),
  312. 'percent': context.util.regex(['calc', 'percent'], 'i'),
  313. 'length': context.util.regex(['length'], 'gi'),
  314. 'xKeyword': /(left|right)/i
  315. }
  316. }
  317. if (value.match(this.cache.xKeyword)) {
  318. value = context.util.swapLeftRight(value)
  319. } else {
  320. var state = context.util.guardFunctions(value)
  321. var parts = state.value.match(this.cache.match)
  322. if (parts && parts.length > 0) {
  323. parts[0] = this.flip(parts[0], context)
  324. state.value = state.value.replace(this.cache.match, function () { return parts.shift() })
  325. value = context.util.unguardFunctions(state)
  326. }
  327. }
  328. return { 'prop': prop, 'value': value }
  329. }
  330. },
  331. {
  332. 'name': 'transform',
  333. 'expr': /^(?!text\-).*?transform$/ig,
  334. 'cache': null,
  335. 'flip': function (value, process, context) {
  336. var i = 0
  337. return value.replace(this.cache.unit, function (num) {
  338. return process(++i, num)
  339. })
  340. },
  341. 'flipMatrix': function (value, context) {
  342. return this.flip(value, function (i, num) {
  343. if (i === 2 || i === 3 || i === 5) {
  344. return context.util.negate(num)
  345. }
  346. return num
  347. }, context)
  348. },
  349. 'flipMatrix3D': function (value, context) {
  350. return this.flip(value, function (i, num) {
  351. if (i === 2 || i === 4 || i === 5 || i === 13) {
  352. return context.util.negate(num)
  353. }
  354. return num
  355. }, context)
  356. },
  357. 'flipRotate3D': function (value, context) {
  358. return this.flip(value, function (i, num) {
  359. if (i === 2 || i === 4) {
  360. return context.util.negate(num)
  361. }
  362. return num
  363. }, context)
  364. },
  365. 'action': function (prop, value, context) {
  366. if (this.cache === null) {
  367. this.cache = {
  368. 'negatable': /((translate)(x|3d)?|rotate(z)?)$/ig,
  369. 'unit': context.util.regex(['calc', 'number'], 'g'),
  370. 'matrix': /matrix$/i,
  371. 'matrix3D': /matrix3d$/i,
  372. 'skewXY': /skew(x|y)?$/i,
  373. 'rotate3D': /rotate3d$/i
  374. }
  375. }
  376. var state = context.util.guardFunctions(value)
  377. return {
  378. 'prop': prop,
  379. 'value': context.util.unguardFunctions(state, function (v, n) {
  380. if (n.length) {
  381. if (n.match(this.cache.matrix3D)) {
  382. v = this.flipMatrix3D(v, context)
  383. } else if (n.match(this.cache.matrix)) {
  384. v = this.flipMatrix(v, context)
  385. } else if (n.match(this.cache.rotate3D)) {
  386. v = this.flipRotate3D(v, context)
  387. } else if (n.match(this.cache.skewXY)) {
  388. v = context.util.negateAll(v)
  389. } else if (n.match(this.cache.negatable)) {
  390. v = context.util.negate(v)
  391. }
  392. }
  393. return v
  394. }.bind(this))
  395. }
  396. }
  397. },
  398. {
  399. 'name': 'transition',
  400. 'expr': /transition(-property)?$/i,
  401. 'action': function (prop, value, context) {
  402. return { 'prop': prop, 'value': context.util.swapLeftRight(value) }
  403. }
  404. },
  405. {
  406. 'name': 'background',
  407. 'expr': /background(-position(-x)?|-image)?$/i,
  408. 'cache': null,
  409. 'flip': function (value, context) {
  410. var state = util.saveTokens(value, true)
  411. var parts = state.value.match(this.cache.match)
  412. if (parts && parts.length > 0) {
  413. var keywords = (state.value.match(this.cache.position) || '').length
  414. if (/* edge offsets */ parts.length >= 3 || /* keywords only */ keywords === 2) {
  415. state.value = util.swapLeftRight(state.value)
  416. } else {
  417. parts[0] = parts[0] === '0'
  418. ? '100%'
  419. : (parts[0].match(this.cache.percent)
  420. ? context.util.complement(parts[0])
  421. : (parts[0].match(this.cache.length)
  422. ? context.util.flipLength(parts[0])
  423. : context.util.swapLeftRight(parts[0])))
  424. state.value = state.value.replace(this.cache.match, function () { return parts.shift() })
  425. }
  426. }
  427. return util.restoreTokens(state)
  428. },
  429. 'update': function (context, value, name) {
  430. if (name.match(this.cache.gradient)) {
  431. value = context.util.swapLeftRight(value)
  432. if (value.match(this.cache.angle)) {
  433. value = context.util.negate(value)
  434. }
  435. } else if (context.config.processUrls === true || context.config.processUrls.decl === true && name.match(this.cache.url)) {
  436. value = context.util.applyStringMap(value, true)
  437. }
  438. return value
  439. },
  440. 'action': function (prop, value, context) {
  441. if (this.cache === null) {
  442. this.cache = {
  443. 'match': context.util.regex(['position', 'percent', 'length', 'calc'], 'ig'),
  444. 'percent': context.util.regex(['calc', 'percent'], 'i'),
  445. 'position': context.util.regex(['position'], 'g'),
  446. 'length': context.util.regex(['length'], 'gi'),
  447. 'gradient': /gradient$/i,
  448. 'angle': /\d+(deg|g?rad|turn)/i,
  449. 'url': /^url/i
  450. }
  451. }
  452. var colorSafe = context.util.guardHexColors(value)
  453. var funcSafe = context.util.guardFunctions(colorSafe.value)
  454. var parts = funcSafe.value.split(',')
  455. var lprop = prop.toLowerCase()
  456. if (lprop !== 'background-image') {
  457. for (var x = 0; x < parts.length; x++) {
  458. parts[x] = this.flip(parts[x], context)
  459. }
  460. }
  461. funcSafe.value = parts.join(',')
  462. colorSafe.value = context.util.unguardFunctions(funcSafe, this.update.bind(this, context))
  463. return {
  464. 'prop': prop,
  465. 'value': context.util.unguardHexColors(colorSafe)
  466. }
  467. }
  468. },
  469. {
  470. 'name': 'keyword',
  471. 'expr': /float|clear|text-align/i,
  472. 'action': function (prop, value, context) {
  473. return { 'prop': prop, 'value': context.util.swapLeftRight(value) }
  474. }
  475. },
  476. {
  477. 'name': 'cursor',
  478. 'expr': /cursor/i,
  479. 'cache': null,
  480. 'update': function (context, value, name) {
  481. if (context.config.processUrls === true || context.config.processUrls.decl === true && name.match(this.cache.url)) {
  482. value = context.util.applyStringMap(value, true)
  483. }
  484. return value
  485. },
  486. 'flip': function (value) {
  487. return value.replace(this.cache.replace, function (s, m) {
  488. return s.replace(m, m.replace(this.cache.e, '*').replace(this.cache.w, 'e').replace(this.cache.star, 'w'))
  489. }.bind(this))
  490. },
  491. 'action': function (prop, value, context) {
  492. if (this.cache === null) {
  493. this.cache = {
  494. 'replace': /\b(ne|nw|se|sw|nesw|nwse)-resize/ig,
  495. 'url': /^url/i,
  496. 'e': /e/i,
  497. 'w': /w/i,
  498. 'star': /\*/i
  499. }
  500. }
  501. var state = context.util.guardFunctions(value)
  502. var parts = state.value.split(',')
  503. for (var x = 0; x < parts.length; x++) {
  504. parts[x] = this.flip(parts[x])
  505. }
  506. state.value = parts.join(',')
  507. return {
  508. 'prop': prop,
  509. 'value': context.util.unguardFunctions(state, this.update.bind(this, context))
  510. }
  511. }
  512. }
  513. ]
  514. }