rtlcss.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. /*
  2. * RTLCSS https://github.com/MohammadYounes/rtlcss
  3. * Framework for transforming Cascading Style Sheets (CSS) from Left-To-Right (LTR) to Right-To-Left (RTL).
  4. * Copyright 2017 Mohammad Younes.
  5. * Licensed under MIT <http://opensource.org/licenses/mit-license.php>
  6. * */
  7. 'use strict'
  8. var postcss = require('postcss')
  9. var state = require('./state.js')
  10. var config = require('./config.js')
  11. var util = require('./util.js')
  12. module.exports = postcss.plugin('rtlcss', function (options, plugins, hooks) {
  13. var configuration = config.configure(options, plugins, hooks)
  14. var context = {
  15. // provides access to postcss
  16. 'postcss': postcss,
  17. // provides access to the current configuration
  18. 'config': configuration,
  19. // provides access to utilities object
  20. 'util': util.configure(configuration)
  21. }
  22. return function (css, result) {
  23. var flipped = 0
  24. var toBeRenamed = {}
  25. context.config.hooks.pre(css, postcss)
  26. css.walk(function (node) {
  27. var prevent = false
  28. state.walk(function (current) {
  29. // check if current directive is expecting this node
  30. if (!current.metadata.blacklist && current.directive.expect[node.type]) {
  31. // perform action and prevent further processing if result equals true
  32. if (current.directive.begin(node, current.metadata, context)) {
  33. prevent = true
  34. }
  35. // if should end? end it.
  36. if (current.metadata.end && current.directive.end(node, current.metadata, context)) {
  37. state.pop(current)
  38. }
  39. }
  40. })
  41. if (prevent === false) {
  42. switch (node.type) {
  43. case 'atrule':
  44. // @rules requires url flipping only
  45. if (context.config.processUrls === true || context.config.processUrls.atrule === true) {
  46. var params = context.util.applyStringMap(node.params, true)
  47. node.params = params
  48. }
  49. break
  50. case 'comment':
  51. state.parse(node, result, function (current) {
  52. var push = true
  53. if (current.directive === null) {
  54. current.preserve = !context.config.clean
  55. context.util.each(context.config.plugins, function (plugin) {
  56. var blacklist = context.config.blacklist[plugin.name]
  57. if (blacklist && blacklist[current.metadata.name] === true) {
  58. current.metadata.blacklist = true
  59. if (current.metadata.end) {
  60. push = false
  61. }
  62. if (current.metadata.begin) {
  63. result.warn('directive "' + plugin.name + '.' + current.metadata.name + '" is blacklisted.', {node: current.source})
  64. }
  65. // break each
  66. return false
  67. }
  68. current.directive = plugin.directives.control[current.metadata.name]
  69. if (current.directive) {
  70. // break each
  71. return false
  72. }
  73. })
  74. }
  75. if (current.directive) {
  76. if (!current.metadata.begin && current.metadata.end) {
  77. if (current.directive.end(node, current.metadata, context)) {
  78. state.pop(current)
  79. }
  80. push = false
  81. } else if (current.directive.expect.self && current.directive.begin(node, current.metadata, context)) {
  82. if (current.metadata.end && current.directive.end(node, current.metadata, context)) {
  83. push = false
  84. }
  85. }
  86. } else if (!current.metadata.blacklist) {
  87. push = false
  88. result.warn('unsupported directive "' + current.metadata.name + '".', {node: current.source})
  89. }
  90. return push
  91. })
  92. break
  93. case 'decl':
  94. // if broken by a matching value directive .. break
  95. if (!context.util.each(context.config.plugins,
  96. function (plugin) {
  97. return context.util.each(plugin.directives.value, function (directive) {
  98. if (node.raws.value && node.raws.value.raw) {
  99. var expr = context.util.regexDirective(directive.name)
  100. if (expr.test(node.raws.value.raw)) {
  101. expr.lastIndex = 0
  102. if (directive.action(node, expr, context)) {
  103. if (context.config.clean) {
  104. node.value = node.raws.value.raw = context.util.trimDirective(node.raws.value.raw)
  105. }
  106. flipped++
  107. // break
  108. return false
  109. }
  110. }
  111. }
  112. })
  113. })) break
  114. // loop over all plugins/property processors
  115. context.util.each(context.config.plugins, function (plugin) {
  116. return context.util.each(plugin.processors, function (processor) {
  117. if (node.prop.match(processor.expr)) {
  118. var raw = node.raws.value && node.raws.value.raw ? node.raws.value.raw : node.value
  119. var state = context.util.saveComments(raw)
  120. var pair = processor.action(node.prop, state.value, context)
  121. state.value = pair.value
  122. pair.value = context.util.restoreComments(state)
  123. if (pair.prop !== node.prop || pair.value !== raw) {
  124. flipped++
  125. node.prop = pair.prop
  126. node.value = pair.value
  127. }
  128. // match found, break
  129. return false
  130. }
  131. })
  132. })
  133. // if last decl, apply auto rename
  134. // decl. may be found inside @rules
  135. if (context.config.autoRename && !flipped && node.parent.type === 'rule' && context.util.isLastOfType(node)) {
  136. var renamed = context.util.applyStringMap(node.parent.selector)
  137. if (context.config.autoRenameStrict === true) {
  138. var pair = toBeRenamed[renamed]
  139. if (pair) {
  140. pair.selector = node.parent.selector
  141. node.parent.selector = renamed
  142. } else {
  143. toBeRenamed[node.parent.selector] = node.parent
  144. }
  145. } else {
  146. node.parent.selector = renamed
  147. }
  148. }
  149. break
  150. case 'rule':
  151. // new rule, reset flipped decl count to zero
  152. flipped = 0
  153. break
  154. }
  155. }
  156. })
  157. state.walk(function (item) {
  158. result.warn('unclosed directive "' + item.metadata.name + '".', {node: item.source})
  159. })
  160. Object.keys(toBeRenamed).forEach(function (key) {
  161. result.warn('renaming skipped due to lack of a matching pair.', {node: toBeRenamed[key]})
  162. })
  163. context.config.hooks.post(css, postcss)
  164. }
  165. })
  166. /**
  167. * Creates a new RTLCSS instance, process the input and return its result.
  168. * @param {String} css A string containing input CSS.
  169. * @param {Object} options An object containing RTLCSS settings.
  170. * @param {Object|Array} plugins An array containing a list of RTLCSS plugins or a single RTLCSS plugin.
  171. * @param {Object} hooks An object containing pre/post hooks.
  172. * @returns {String} A string contining the RTLed css.
  173. */
  174. module.exports.process = function (css, options, plugins, hooks) {
  175. return postcss([this(options, plugins, hooks)]).process(css).css
  176. }
  177. /**
  178. * Creates a new instance of RTLCSS using the passed configuration object
  179. * @param {Object} config An object containing RTLCSS options, plugins and hooks.
  180. * @returns {Object} A new RTLCSS instance.
  181. */
  182. module.exports.configure = function (config) {
  183. config = config || {}
  184. return postcss([this(config.options, config.plugins, config.hooks)])
  185. }