diff --git a/.npmignore b/.npmignore index b7903331..c388c81c 100644 --- a/.npmignore +++ b/.npmignore @@ -1,3 +1,4 @@ node_modules coverage -test \ No newline at end of file +test +example diff --git a/.travis.yml b/.travis.yml index 6bcd11f5..071c5b3d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,9 +4,6 @@ node_js: - "0.12" - "node" - "iojs" -script: npm run travis -after_success: - - cat ./coverage/lcov.info | node_modules/.bin/coveralls --verbose - - cat ./coverage/coverage.json | node_modules/codecov.io/bin/codecov.io.js - - rm -rf ./coverage +notifications: + email: false diff --git a/README.md b/README.md index f8b85b19..ab7877b1 100644 --- a/README.md +++ b/README.md @@ -1,259 +1,36 @@ -# css loader for webpack +# (rtl) css loader for webpack [](https://travis-ci.org/romainberger/rtl-css-loader) -## installation +Fork of the [css loader](https://github.com/webpack/css-loader), improved for rtl. -`npm install css-loader --save-dev` +Drop-in replacement of the css-loader, it simply checks the `dir` attribute on the `html` tag on the page, then injects either the regular css or the rtl'ized css. -## Usage +Uses [rtlcss](https://github.com/MohammadYounes/rtlcss) under the hood. -[Documentation: Using loaders](http://webpack.github.io/docs/using-loaders.html) +**Warning** This module should only be used for development. The processing of your css being done server-side, the bundle will include both the regular css and the rtl version, which can make your bundle a lot bigger. If you're using the `extract-text-webpack-plugin`, check out the [webpack-rtl-plugin](https://github.com/romainberger/webpack-rtl-plugin). -``` javascript -var css = require("css!./file.css"); -// => returns css code from file.css, resolves imports and url(...) -``` +Check out the [webpack-rtl-example](https://github.com/romainberger/webpack-rtl-example) to see an example of an app using the rtl-css-loader and webpack-rtl-plugin. -`@import` and `url(...)` are interpreted like `require()` and will be resolved by the css-loader. -Good loaders for requiring your assets are the [file-loader](https://github.com/webpack/file-loader) -and the [url-loader](https://github.com/webpack/url-loader) which you should specify in your config (see below). +## installation -To be compatible with existing css files (if not in CSS Module mode): -* `url(image.png)` => `require("./image.png")` -* `url(~module/image.png)` => `require("module/image.png")` +`npm install rtl-css-loader --save-dev` -### Example config +## Usage -This webpack config can load css files, embed small png images as Data Urls and jpg images as files. +Use it exactly like the [css-loader](https://github.com/webpack/css-loader): ``` javascript module.exports = { module: { loaders: [ - { test: /\.css$/, loader: "style-loader!css-loader" }, - { test: /\.png$/, loader: "url-loader?limit=100000" }, - { test: /\.jpg$/, loader: "file-loader" } + { + test: /\.css$/, + loaders: ['style', 'rtl-css'] + }, ] } }; ``` -### 'Root-relative' urls - -For urls that start with a `/`, the default behavior is to not translate them: -* `url(/image.png)` => `url(/image.png)` - -If a `root` query parameter is set, however, it will be prepended to the url -and then translated: - -With a config like: - -``` javascript - loaders: [ - { test: /\.css$/, loader: "style-loader!css-loader?root=." }, - ... - ] -``` - -The result is: - -* `url(/image.png)` => `require("./image.png")` - -Using 'Root-relative' urls is not recommended. You should only use it for legacy CSS files. - -### Local scope - -By default CSS exports all class names into a global selector scope. This is a feature which offer a local selector scope. - -The syntax `:local(.className)` can be used to declare `className` in the local scope. The local identifiers are exported by the module. - -With `:local` (without brackets) local mode can be switched on for this selector. `:global(.className)` can be used to declare an explicit global selector. With `:global` (without brackets) global mode can be switched on for this selector. - -The loader replaces local selectors with unique identifiers. The choosen unique identifiers are exported by the module. - -Example: - -``` css -:local(.className) { background: red; } -:local .className { color: green; } -:local(.className .subClass) { color: green; } -:local .className .subClass :global(.global-class-name) { color: blue; } -``` - -is transformed to - -``` css -._23_aKvs-b8bW2Vg3fwHozO { background: red; } -._23_aKvs-b8bW2Vg3fwHozO { color: green; } -._23_aKvs-b8bW2Vg3fwHozO ._13LGdX8RMStbBE9w-t0gZ1 { color: green; } -._23_aKvs-b8bW2Vg3fwHozO ._13LGdX8RMStbBE9w-t0gZ1 .global-class-name { color: blue; } -``` - -and the identifiers are exported: - -``` js -exports.locals = { - className: "_23_aKvs-b8bW2Vg3fwHozO", - subClass: "_13LGdX8RMStbBE9w-t0gZ1" -} -``` - -Camelcasing is recommended for local selectors. They are easier to use in the importing javascript module. - -`url(...)` URLs in block scoped (`:local .abc`) rules behave like requests in modules: - * `./file.png` instead of `file.png` - * `module/file.png` instead of `~module/file.png` - - -You can use `:local(#someId)`, but this is not recommended. Use classes instead of ids. - -You can configure the generated ident with the `localIdentName` query parameter (default `[hash:base64]`). Example: `css-loader?localIdentName=[path][name]---[local]---[hash:base64:5]` for easier debugging. - -Note: For prerendering with extract-text-webpack-plugin you should use `css-loader/locals` instead of `style-loader!css-loader` **in the prerendering bundle**. It doesn't embed CSS but only exports the identifier mappings. - -### CSS Modules - -See [CSS Modules](https://github.com/css-modules/css-modules). - -The query parameter `modules` enables the **CSS Modules** spec. (`css-loader?modules`) - -This enables Local scoped CSS by default. (You can switch it off with `:global(...)` or `:global` for selectors and/or rules.) - -### Composing CSS classes - -When declaring a local class name you can compose a local class from another local class name. - -``` css -:local(.className) { - background: red; - color: yellow; -} - -:local(.subClass) { - composes: className; - background: blue; -} -``` - -This doesn't result in any change to the CSS itself but exports multiple class names: - -``` js -exports.locals = { - className: "_23_aKvs-b8bW2Vg3fwHozO", - subClass: "_13LGdX8RMStbBE9w-t0gZ1 _23_aKvs-b8bW2Vg3fwHozO" -} -``` - -and CSS is transformed to: - -``` css -._23_aKvs-b8bW2Vg3fwHozO { - background: red; - color: yellow; -} - -._13LGdX8RMStbBE9w-t0gZ1 { - background: blue; -} -``` - -### Importing local class names - -To import a local class name from another module: - -``` css -:local(.continueButton) { - composes: button from "library/button.css"; - background: red; -} -``` - -``` css -:local(.nameEdit) { - composes: edit highlight from "./edit.css"; - background: red; -} -``` - -To import from multiple modules use multiple `composes:` rules. - -``` css -:local(.className) { - composes: edit hightlight from "./edit.css"; - composes: button from "module/button.css"; - composes: classFromThisModule; - background: red; -} -``` - -### SourceMaps - -To include SourceMaps set the `sourceMap` query param. - -`require("css-loader?sourceMap!./file.css")` - -I. e. the extract-text-webpack-plugin can handle them. - -They are not enabled by default because they expose a runtime overhead and increase in bundle size (JS SourceMap do not). In addition to that relative paths are buggy and you need to use an absolute public path which include the server url. - -### importing and chained loaders - -The query parameter `importLoaders` allow to configure which loaders should be applied to `@import`ed resources. - -`importLoaders` (int): That many loaders after the css-loader are used to import resources. - -Examples: - -``` js -require("style-loader!css-loader?importLoaders=1!autoprefixer-loader!...") -// => imported resources are handled this way: -require("css-loader?importLoaders=1!autoprefixer-loader!...") - -require("style-loader!css-loader!stylus-loader!...") -// => imported resources are handled this way: -require("css-loader!...") -``` - -This may change in the future, when the module system (i. e. webpack) supports loader matching by origin. - -### Minification - -By default the css-loader minimizes the css if specified by the module system. - -In some cases the minification is destructive to the css, so you can provide some options to it. cssnano is used for minification and you find a [list of options here](http://cssnano.co/options/). Just provide them as query parameter: i. e. `require("css-loader?-autoprefixer")` to disable removing of deprecated vendor prefixes. - -You can also disable or enforce minification with the `minimize` query parameter. - -`require("css-loader?minimize!./file.css")` (enforced) - -`require("css-loader?-minimize!./file.css")` (disabled) - -### Disable behavior - -`css-loader?-url` disables `url(...)` handling. - -`css-loader?-import` disables `@import` handling. - -### Camel case - -By default, the exported JSON keys mirror the class names. If you want to camelize class names (useful in Javascript), pass the query parameter `camelCase` to the loader. - -Example: - -`css-loader?camelCase` - -Usage: -```css -/* file.css */ - -.class-name { /* ... */ } -``` - -```js -// javascript - -require('file.css').className -``` - ## License -MIT (http://www.opensource.org/licenses/mit-license.php) +[MIT](http://www.opensource.org/licenses/mit-license.php) diff --git a/example/README.md b/example/README.md new file mode 100644 index 00000000..222979d6 --- /dev/null +++ b/example/README.md @@ -0,0 +1,23 @@ +# rtl-css-loader example + +# Usage + +```shell +# install the module dependencies +cd .. +npm install + +# install the example dependencies +cd example +npm install + +# run the webpack compilation +npm run webpack + +# start the development server (uses python) +npm start +``` + +Then open your browser at [http://localhost:3000](http://localhost:3000). + +To see how the module works, open `index.html` then change the `dir` attribute on the `html` tag. diff --git a/example/dist/app.js b/example/dist/app.js new file mode 100644 index 00000000..95cf4fa5 --- /dev/null +++ b/example/dist/app.js @@ -0,0 +1,414 @@ +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; + +/******/ // The require function +/******/ function __webpack_require__(moduleId) { + +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) +/******/ return installedModules[moduleId].exports; + +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ exports: {}, +/******/ id: moduleId, +/******/ loaded: false +/******/ }; + +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); + +/******/ // Flag the module as loaded +/******/ module.loaded = true; + +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } + + +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; + +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; + +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; + +/******/ // Load entry module and return exports +/******/ return __webpack_require__(0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ function(module, exports, __webpack_require__) { + + __webpack_require__(1) + console.log('Hey wassup') + + +/***/ }, +/* 1 */ +/***/ function(module, exports, __webpack_require__) { + + // style-loader: Adds some css to the DOM by adding a tag + + // load the styles + var content = __webpack_require__(2); + if(typeof content === 'string') content = [[module.id, content, '']]; + // add the styles to the DOM + var update = __webpack_require__(5)(content, {}); + if(content.locals) module.exports = content.locals; + // Hot Module Replacement + if(false) { + // When the styles change, update the tags + if(!content.locals) { + module.hot.accept("!!./../index.js!./index.css", function() { + var newContent = require("!!./../index.js!./index.css"); + if(typeof newContent === 'string') newContent = [[module.id, newContent, '']]; + update(newContent); + }); + } + // When the module is disposed, remove the tags + module.hot.dispose(function() { update(); }); + } + +/***/ }, +/* 2 */ +/***/ function(module, exports, __webpack_require__) { + + exports = module.exports = __webpack_require__(3)(); + // imports + exports.i(__webpack_require__(4), ""); + + // module + exports.push([module.id, document.getElementsByTagName('html')[0].getAttribute('dir') === 'rtl' ? "body {\n background: #eee;\n}\n\n#root {\n position: absolute;\n right: 50px;\n}\n" : "body {\n background: #eee;\n}\n\n#root {\n position: absolute;\n left: 50px;\n}\n", ""]); + + // exports + + +/***/ }, +/* 3 */ +/***/ function(module, exports) { + + /* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra + */ + // css base code, injected by the css-loader + module.exports = function() { + var list = []; + + // return the list of modules as css string + list.toString = function toString() { + var result = []; + for(var i = 0; i < this.length; i++) { + var item = this[i]; + if(item[2]) { + result.push("@media " + item[2] + "{" + item[1] + "}"); + } else { + result.push(item[1]); + } + } + return result.join(""); + }; + + // import a list of modules into the list + list.i = function(modules, mediaQuery) { + if(typeof modules === "string") + modules = [[null, modules, ""]]; + var alreadyImportedModules = {}; + for(var i = 0; i < this.length; i++) { + var id = this[i][0]; + if(typeof id === "number") + alreadyImportedModules[id] = true; + } + for(i = 0; i < modules.length; i++) { + var item = modules[i]; + // skip already imported module + // this implementation is not 100% perfect for weird media query combinations + // when a module is imported multiple times with different media queries. + // I hope this will never occur (Hey this way we have smaller bundles) + if(typeof item[0] !== "number" || !alreadyImportedModules[item[0]]) { + if(mediaQuery && !item[2]) { + item[2] = mediaQuery; + } else if(mediaQuery) { + item[2] = "(" + item[2] + ") and (" + mediaQuery + ")"; + } + list.push(item); + } + } + }; + return list; + }; + + +/***/ }, +/* 4 */ +/***/ function(module, exports, __webpack_require__) { + + exports = module.exports = __webpack_require__(3)(); + // imports + + + // module + exports.push([module.id, document.getElementsByTagName('html')[0].getAttribute('dir') === 'rtl' ? "body {\n color: #00f;\n}\n" : "body {\n color: #00f;\n}\n", ""]); + + // exports + + +/***/ }, +/* 5 */ +/***/ function(module, exports, __webpack_require__) { + + /* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra + */ + var stylesInDom = {}, + memoize = function(fn) { + var memo; + return function () { + if (typeof memo === "undefined") memo = fn.apply(this, arguments); + return memo; + }; + }, + isOldIE = memoize(function() { + return /msie [6-9]\b/.test(window.navigator.userAgent.toLowerCase()); + }), + getHeadElement = memoize(function () { + return document.head || document.getElementsByTagName("head")[0]; + }), + singletonElement = null, + singletonCounter = 0, + styleElementsInsertedAtTop = []; + + module.exports = function(list, options) { + if(false) { + if(typeof document !== "object") throw new Error("The style-loader cannot be used in a non-browser environment"); + } + + options = options || {}; + // Force single-tag solution on IE6-9, which has a hard limit on the # of + // tags it will allow on a page + if (typeof options.singleton === "undefined") options.singleton = isOldIE(); + + // By default, add tags to the bottom of
. + if (typeof options.insertAt === "undefined") options.insertAt = "bottom"; + + var styles = listToStyles(list); + addStylesToDom(styles, options); + + return function update(newList) { + var mayRemove = []; + for(var i = 0; i < styles.length; i++) { + var item = styles[i]; + var domStyle = stylesInDom[item.id]; + domStyle.refs--; + mayRemove.push(domStyle); + } + if(newList) { + var newStyles = listToStyles(newList); + addStylesToDom(newStyles, options); + } + for(var i = 0; i < mayRemove.length; i++) { + var domStyle = mayRemove[i]; + if(domStyle.refs === 0) { + for(var j = 0; j < domStyle.parts.length; j++) + domStyle.parts[j](); + delete stylesInDom[domStyle.id]; + } + } + }; + } + + function addStylesToDom(styles, options) { + for(var i = 0; i < styles.length; i++) { + var item = styles[i]; + var domStyle = stylesInDom[item.id]; + if(domStyle) { + domStyle.refs++; + for(var j = 0; j < domStyle.parts.length; j++) { + domStyle.parts[j](item.parts[j]); + } + for(; j < item.parts.length; j++) { + domStyle.parts.push(addStyle(item.parts[j], options)); + } + } else { + var parts = []; + for(var j = 0; j < item.parts.length; j++) { + parts.push(addStyle(item.parts[j], options)); + } + stylesInDom[item.id] = {id: item.id, refs: 1, parts: parts}; + } + } + } + + function listToStyles(list) { + var styles = []; + var newStyles = {}; + for(var i = 0; i < list.length; i++) { + var item = list[i]; + var id = item[0]; + var css = item[1]; + var media = item[2]; + var sourceMap = item[3]; + var part = {css: css, media: media, sourceMap: sourceMap}; + if(!newStyles[id]) + styles.push(newStyles[id] = {id: id, parts: [part]}); + else + newStyles[id].parts.push(part); + } + return styles; + } + + function insertStyleElement(options, styleElement) { + var head = getHeadElement(); + var lastStyleElementInsertedAtTop = styleElementsInsertedAtTop[styleElementsInsertedAtTop.length - 1]; + if (options.insertAt === "top") { + if(!lastStyleElementInsertedAtTop) { + head.insertBefore(styleElement, head.firstChild); + } else if(lastStyleElementInsertedAtTop.nextSibling) { + head.insertBefore(styleElement, lastStyleElementInsertedAtTop.nextSibling); + } else { + head.appendChild(styleElement); + } + styleElementsInsertedAtTop.push(styleElement); + } else if (options.insertAt === "bottom") { + head.appendChild(styleElement); + } else { + throw new Error("Invalid value for parameter 'insertAt'. Must be 'top' or 'bottom'."); + } + } + + function removeStyleElement(styleElement) { + styleElement.parentNode.removeChild(styleElement); + var idx = styleElementsInsertedAtTop.indexOf(styleElement); + if(idx >= 0) { + styleElementsInsertedAtTop.splice(idx, 1); + } + } + + function createStyleElement(options) { + var styleElement = document.createElement("style"); + styleElement.type = "text/css"; + insertStyleElement(options, styleElement); + return styleElement; + } + + function createLinkElement(options) { + var linkElement = document.createElement("link"); + linkElement.rel = "stylesheet"; + insertStyleElement(options, linkElement); + return linkElement; + } + + function addStyle(obj, options) { + var styleElement, update, remove; + + if (options.singleton) { + var styleIndex = singletonCounter++; + styleElement = singletonElement || (singletonElement = createStyleElement(options)); + update = applyToSingletonTag.bind(null, styleElement, styleIndex, false); + remove = applyToSingletonTag.bind(null, styleElement, styleIndex, true); + } else if(obj.sourceMap && + typeof URL === "function" && + typeof URL.createObjectURL === "function" && + typeof URL.revokeObjectURL === "function" && + typeof Blob === "function" && + typeof btoa === "function") { + styleElement = createLinkElement(options); + update = updateLink.bind(null, styleElement); + remove = function() { + removeStyleElement(styleElement); + if(styleElement.href) + URL.revokeObjectURL(styleElement.href); + }; + } else { + styleElement = createStyleElement(options); + update = applyToTag.bind(null, styleElement); + remove = function() { + removeStyleElement(styleElement); + }; + } + + update(obj); + + return function updateStyle(newObj) { + if(newObj) { + if(newObj.css === obj.css && newObj.media === obj.media && newObj.sourceMap === obj.sourceMap) + return; + update(obj = newObj); + } else { + remove(); + } + }; + } + + var replaceText = (function () { + var textStore = []; + + return function (index, replacement) { + textStore[index] = replacement; + return textStore.filter(Boolean).join('\n'); + }; + })(); + + function applyToSingletonTag(styleElement, index, remove, obj) { + var css = remove ? "" : obj.css; + + if (styleElement.styleSheet) { + styleElement.styleSheet.cssText = replaceText(index, css); + } else { + var cssNode = document.createTextNode(css); + var childNodes = styleElement.childNodes; + if (childNodes[index]) styleElement.removeChild(childNodes[index]); + if (childNodes.length) { + styleElement.insertBefore(cssNode, childNodes[index]); + } else { + styleElement.appendChild(cssNode); + } + } + } + + function applyToTag(styleElement, obj) { + var css = obj.css; + var media = obj.media; + + if(media) { + styleElement.setAttribute("media", media) + } + + if(styleElement.styleSheet) { + styleElement.styleSheet.cssText = css; + } else { + while(styleElement.firstChild) { + styleElement.removeChild(styleElement.firstChild); + } + styleElement.appendChild(document.createTextNode(css)); + } + } + + function updateLink(linkElement, obj) { + var css = obj.css; + var sourceMap = obj.sourceMap; + + if(sourceMap) { + // http://stackoverflow.com/a/26603875 + css += "\n/*# sourceMappingURL=data:application/json;base64," + btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap)))) + " */"; + } + + var blob = new Blob([css], { type: "text/css" }); + + var oldSrc = linkElement.href; + + linkElement.href = URL.createObjectURL(blob); + + if(oldSrc) + URL.revokeObjectURL(oldSrc); + } + + +/***/ } +/******/ ]); \ No newline at end of file diff --git a/example/index.css b/example/index.css new file mode 100644 index 00000000..33d95fbd --- /dev/null +++ b/example/index.css @@ -0,0 +1,10 @@ +@import "./second.css"; + +body { + background: #eee; +} + +#root { + position: absolute; + left: 50px; +} diff --git a/example/index.html b/example/index.html new file mode 100644 index 00000000..6b54d4b2 --- /dev/null +++ b/example/index.html @@ -0,0 +1,10 @@ + + + +