/** * RTLCSS https://github.com/MohammadYounes/rtlcss * Framework for transforming Cascading Style Sheets (CSS) from Left-To-Right (LTR) to Right-To-Left (RTL). * Copyright 2017 Mohammad Younes. * Licensed under MIT */ 'use strict' const postcss = require('postcss') const state = require('./state.js') const config = require('./config.js') const util = require('./util.js') module.exports = (options, plugins, hooks) => { const processed = Symbol('processed') const configuration = config.configure(options, plugins, hooks) const context = { // provides access to postcss postcss, // provides access to the current configuration config: configuration, // provides access to utilities object util: util.configure(configuration), // processed symbol symbol: processed } let flipped = 0 const toBeRenamed = {} function shouldProcess (node, result) { if (!node[processed]) { let prevent = false state.walk((current) => { // check if current directive is expecting this node if (!current.metadata.blacklist && current.directive.expect[node.type]) { // perform action and prevent further processing if result equals true if (current.directive.begin(node, current.metadata, context)) { prevent = true } // if should end? end it. if (current.metadata.end && current.directive.end(node, current.metadata, context)) { state.pop(current) } } }) node[processed] = true return !prevent } return false } return { postcssPlugin: 'rtlcss', Once (root, { result }) { context.config.hooks.pre(root, postcss) shouldProcess(root, result) }, Rule (node, { result }) { if (shouldProcess(node, result)) { // new rule, reset flipped decl count to zero flipped = 0 } }, AtRule (node, { result }) { if (shouldProcess(node, result) && // @rules requires url flipping only (context.config.processUrls === true || context.config.processUrls.atrule === true) ) { const params = context.util.applyStringMap(node.params, true) node.params = params } }, Comment (node, { result }) { if (shouldProcess(node, result)) { state.parse(node, result, (current) => { let push = true if (current.directive === null) { current.preserve = !context.config.clean context.util.each(context.config.plugins, (plugin) => { const blacklist = context.config.blacklist[plugin.name] if (blacklist && blacklist[current.metadata.name] === true) { current.metadata.blacklist = true if (current.metadata.end) { push = false } if (current.metadata.begin) { result.warn(`directive "${plugin.name}.${current.metadata.name}" is blacklisted.`, { node: current.source }) } // break each return false } current.directive = plugin.directives.control[current.metadata.name] if (current.directive) { // break each return false } }) } if (current.directive) { if (!current.metadata.begin && current.metadata.end) { if (current.directive.end(node, current.metadata, context)) { state.pop(current) } push = false } else if ( current.directive.expect.self && current.directive.begin(node, current.metadata, context) && current.metadata.end && current.directive.end(node, current.metadata, context) ) { push = false } } else if (!current.metadata.blacklist) { push = false result.warn(`unsupported directive "${current.metadata.name}".`, { node: current.source }) } return push }) } }, Declaration (node, { result }) { if (shouldProcess(node, result)) { // if broken by a matching value directive .. break if (!context.util.each(context.config.plugins, (plugin) => { return context.util.each(plugin.directives.value, (directive) => { const hasRawValue = node.raws.value && node.raws.value.raw const expr = context.util.regexDirective(directive.name) if (expr.test(`${node.raws.between}${hasRawValue ? node.raws.value.raw : ''}${node.important && node.raws.important ? node.raws.important : ''}`)) { expr.lastIndex = 0 if (directive.action(node, expr, context)) { if (context.config.clean) { node.raws.between = context.util.trimDirective(node.raws.between) if (node.important && node.raws.important) { node.raws.important = context.util.trimDirective(node.raws.important) } if (hasRawValue) { node.value = node.raws.value.raw = context.util.trimDirective(node.raws.value.raw) } } flipped++ // break return false } } }) })) return // loop over all plugins/property processors context.util.each(context.config.plugins, (plugin) => { return context.util.each(plugin.processors, (processor) => { const alias = context.config.aliases[node.prop] if ((alias || node.prop).match(processor.expr)) { const raw = node.raws.value && node.raws.value.raw ? node.raws.value.raw : node.value const state = context.util.saveComments(raw) if (context.config.processEnv) { state.value = context.util.swap(state.value, 'safe-area-inset-left', 'safe-area-inset-right', { ignoreCase: false }) } const pair = processor.action(node.prop, state.value, context) state.value = pair.value pair.value = context.util.restoreComments(state) if ((!alias && pair.prop !== node.prop) || pair.value !== raw) { flipped++ node.prop = pair.prop node.value = pair.value } // match found, break return false } }) }) // if last decl, apply auto rename // decl. may be found inside @rules if (context.config.autoRename && !flipped && node.parent.type === 'rule' && context.util.isLastOfType(node)) { const renamed = context.util.applyStringMap(node.parent.selector) if (context.config.autoRenameStrict === true) { const pair = toBeRenamed[renamed] if (pair) { pair.selector = node.parent.selector node.parent.selector = renamed } else { toBeRenamed[node.parent.selector] = node.parent } } else { node.parent.selector = renamed } } } }, OnceExit (root, { result }) { state.walk((item) => { result.warn(`unclosed directive "${item.metadata.name}".`, { node: item.source }) }) for (const key of Object.keys(toBeRenamed)) { result.warn('renaming skipped due to lack of a matching pair.', { node: toBeRenamed[key] }) } context.config.hooks.post(root, postcss) } } } module.exports.postcss = true /** * Creates a new RTLCSS instance, process the input and return its result. * @param {String} css A string containing input CSS. * @param {Object} options An object containing RTLCSS settings. * @param {Object|Array} plugins An array containing a list of RTLCSS plugins or a single RTLCSS plugin. * @param {Object} hooks An object containing pre/post hooks. * @returns {String} A string contining the RTLed css. */ module.exports.process = function (css, options, plugins, hooks) { return postcss([this(options, plugins, hooks)]).process(css).css } /** * Creates a new instance of RTLCSS using the passed configuration object * @param {Object} config An object containing RTLCSS options, plugins and hooks. * @returns {Object} A new RTLCSS instance. */ module.exports.configure = function (config) { config = config || {} return postcss([this(config.options, config.plugins, config.hooks)]) }