├── test ├── scss │ ├── import.scss │ ├── page.scss │ ├── charset.scss │ ├── params.scss │ ├── return.scss │ ├── variable-parameter.scss │ ├── supports.scss │ ├── namescope.scss │ ├── boolean.scss │ ├── each.scss │ ├── extend.scss │ ├── selector.scss │ ├── font-face.scss │ ├── variables.scss │ ├── media.scss │ ├── if.scss │ ├── variable-search.scss │ ├── keyframes.scss │ ├── mixins.scss │ ├── object.scss │ ├── functions.scss │ ├── comment.scss │ └── autoprefixer-keyframes.scss ├── stylus │ ├── import.styl │ ├── params.styl │ ├── page.styl │ ├── return.styl │ ├── variable-parameter.styl │ ├── charset.styl │ ├── supports.styl │ ├── each.styl │ ├── boolean.styl │ ├── namescope.styl │ ├── selector.styl │ ├── extend.styl │ ├── font-face.styl │ ├── variables.styl │ ├── media.styl │ ├── variable-search.styl │ ├── if.styl │ ├── keyframes.styl │ ├── mixins.styl │ ├── object.styl │ ├── functions.styl │ └── comment.styl ├── vue │ ├── scss │ │ ├── empty.vue │ │ ├── basic.vue │ │ ├── scoped.vue │ │ ├── indented.vue │ │ ├── deep.vue │ │ └── multiple-style-blocks.vue │ └── stylus │ │ ├── empty.vue │ │ ├── basic.vue │ │ ├── indented.vue │ │ ├── scoped.vue │ │ ├── deep.vue │ │ └── multiple-style-blocks.vue ├── test.styl ├── test.scss └── index.spec.js ├── .travis.yml ├── .DS_Store ├── .gitignore ├── banner.png ├── .babelrc ├── rollup.config.js ├── bin ├── convertVueFile.js ├── findMixin.js ├── conver.js ├── convertStylus.js └── file.js ├── LICENSE ├── src ├── index.js ├── util.js └── visitor │ └── index.js ├── package.json ├── doc └── zh-cn.md ├── README.md └── lib └── index.js /test/scss/import.scss: -------------------------------------------------------------------------------- 1 | @import 'mixin.scss'; 2 | -------------------------------------------------------------------------------- /test/stylus/import.styl: -------------------------------------------------------------------------------- 1 | @import 'mixin.styl' 2 | -------------------------------------------------------------------------------- /test/stylus/params.styl: -------------------------------------------------------------------------------- 1 | add(arg) 2 | arg 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" 4 | -------------------------------------------------------------------------------- /test/stylus/page.styl: -------------------------------------------------------------------------------- 1 | @page :first 2 | margin-left: 50% 3 | -------------------------------------------------------------------------------- /test/stylus/return.styl: -------------------------------------------------------------------------------- 1 | returnFn(args) 2 | return args 3 | -------------------------------------------------------------------------------- /test/stylus/variable-parameter.styl: -------------------------------------------------------------------------------- 1 | add(arg...) 2 | arg 3 | -------------------------------------------------------------------------------- /test/scss/page.scss: -------------------------------------------------------------------------------- 1 | @page :first { 2 | margin-left: 50%; 3 | } 4 | -------------------------------------------------------------------------------- /test/scss/charset.scss: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | @charset 'iso-8859-15'; 3 | -------------------------------------------------------------------------------- /test/scss/params.scss: -------------------------------------------------------------------------------- 1 | @function add($arg) { 2 | @return $arg 3 | } 4 | -------------------------------------------------------------------------------- /test/stylus/charset.styl: -------------------------------------------------------------------------------- 1 | @charset "UTF-8" 2 | @charset 'iso-8859-15' 3 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txs1992/stylus-converter/HEAD/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /lib 3 | /test/test.styl 4 | /test/test.scss 5 | -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txs1992/stylus-converter/HEAD/banner.png -------------------------------------------------------------------------------- /test/scss/return.scss: -------------------------------------------------------------------------------- 1 | @function returnFn($args) { 2 | @return $args 3 | } 4 | -------------------------------------------------------------------------------- /test/scss/variable-parameter.scss: -------------------------------------------------------------------------------- 1 | @function add($arg...) { 2 | @return $arg 3 | } 4 | -------------------------------------------------------------------------------- /test/stylus/supports.styl: -------------------------------------------------------------------------------- 1 | @supports (animation-name: test) 2 | div 3 | color: red 4 | -------------------------------------------------------------------------------- /test/scss/supports.scss: -------------------------------------------------------------------------------- 1 | @Supports (animation-name: test) { 2 | div { 3 | color: red; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/stylus/each.styl: -------------------------------------------------------------------------------- 1 | div 2 | for num in (1..5) 3 | foo num 4 | for str in 1 2 3 4 5 5 | bar str 6 | -------------------------------------------------------------------------------- /test/scss/namescope.scss: -------------------------------------------------------------------------------- 1 | @namespace 'http://www.w3.org/1999/xhtml'; 2 | @namespace url(http://www.w3.org/1999/xhtml); 3 | -------------------------------------------------------------------------------- /test/stylus/boolean.styl: -------------------------------------------------------------------------------- 1 | a = 10 2 | b = 5 3 | 4 | div 5 | if (a > b && a - b >= b || a - b == b) 6 | color red 7 | -------------------------------------------------------------------------------- /test/stylus/namescope.styl: -------------------------------------------------------------------------------- 1 | @namespace 'http://www.w3.org/1999/xhtml' 2 | @namespace url(http://www.w3.org/1999/xhtml) 3 | -------------------------------------------------------------------------------- /test/stylus/selector.styl: -------------------------------------------------------------------------------- 1 | #app 2 | color: red 3 | 4 | div, span 5 | width: 100px 6 | 7 | ul, & 8 | padding: 10px 9 | -------------------------------------------------------------------------------- /test/stylus/extend.styl: -------------------------------------------------------------------------------- 1 | .message 2 | padding 10px 3 | border 1px solid #eee 4 | 5 | .warning 6 | @extend .message 7 | color #E2E21E 8 | -------------------------------------------------------------------------------- /test/scss/boolean.scss: -------------------------------------------------------------------------------- 1 | $a: 10; 2 | $b: 5; 3 | 4 | div { 5 | @if $a > $b and ($a - $b >= $b or ($a - $b == $b)) { 6 | color: red; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/scss/each.scss: -------------------------------------------------------------------------------- 1 | div { 2 | @for $num from 1 through 5 { 3 | foo: $num; 4 | } 5 | @each $str in 1, 2, 3, 4, 5 { 6 | bar: $str; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/scss/extend.scss: -------------------------------------------------------------------------------- 1 | .message { 2 | padding: 10px; 3 | border: 1px solid #eee; 4 | } 5 | 6 | .warning { 7 | @extend .message; 8 | color: #E2E21E; 9 | } 10 | -------------------------------------------------------------------------------- /test/stylus/font-face.styl: -------------------------------------------------------------------------------- 1 | @font-face 2 | font-family Geo 3 | font-style normal 4 | src url(fonts/geo_sans_light/GensansLight.ttf) 5 | 6 | .ingeo 7 | font-family Geo 8 | -------------------------------------------------------------------------------- /test/vue/scss/empty.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/scss/selector.scss: -------------------------------------------------------------------------------- 1 | #app { 2 | color: red; 3 | 4 | div, span { 5 | width: 100px; 6 | 7 | ul, & { 8 | padding: 10px; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/vue/stylus/empty.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "modules": false 7 | } 8 | ] 9 | ], 10 | "plugins": [ 11 | "external-helpers" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /test/scss/font-face.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: Geo; 3 | font-style: normal; 4 | src: url(fonts/geo_sans_light/GensansLight.ttf); 5 | } 6 | 7 | .ingeo { 8 | font-family: Geo; 9 | } 10 | -------------------------------------------------------------------------------- /test/stylus/variables.styl: -------------------------------------------------------------------------------- 1 | keyframe-name = pulse 2 | default-width = 200px 3 | $val = 20 4 | 5 | div 6 | borde = 1px solid #ccc 7 | border borde 8 | 9 | .test 10 | right 10px 11 | text-align right -------------------------------------------------------------------------------- /test/vue/stylus/basic.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /test/vue/scss/basic.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /test/vue/stylus/indented.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /test/vue/stylus/scoped.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /test/vue/scss/scoped.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /test/vue/scss/indented.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /test/vue/stylus/deep.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /test/scss/variables.scss: -------------------------------------------------------------------------------- 1 | $keyframe-name: pulse; 2 | $default-width: 200px; 3 | $val: 20; 4 | 5 | div { 6 | $borde: 1px solid #ccc; 7 | border: $borde; 8 | } 9 | 10 | .test { 11 | right: 10px; 12 | text-align: right; 13 | } 14 | -------------------------------------------------------------------------------- /test/vue/scss/deep.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 16 | -------------------------------------------------------------------------------- /test/stylus/media.styl: -------------------------------------------------------------------------------- 1 | @media print 2 | display none 3 | 4 | @media screen and (max-width: 500px) and (min-width: 100px), (max-width: 500px) and (min-height: 200px) 5 | .foo 6 | color: #100 7 | 8 | .foo 9 | for i in 1..4 10 | @media (min-width: 2 * (i + 7) px) 11 | width: 100px*i 12 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel' 2 | 3 | export default { 4 | input: './src/index.js', 5 | output: { 6 | file: './lib/index.js', 7 | format: 'cjs' 8 | }, 9 | plugins: [ 10 | babel({ 11 | exclude: 'node_modules/**', 12 | plugins: ['external-helpers'] 13 | }) 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /test/stylus/variable-search.styl: -------------------------------------------------------------------------------- 1 | #logo 2 | width: w = 150px 3 | height: h = 80px 4 | margin-left: -(w / 2) 5 | margin-top: -(h / 2) 6 | 7 | /* property lookup */ 8 | div 9 | width: @width 10 | 11 | body 12 | color: red 13 | ul 14 | li 15 | color: blue 16 | a 17 | /* property up bubble lookup */ 18 | background-color: @color 19 | -------------------------------------------------------------------------------- /test/scss/media.scss: -------------------------------------------------------------------------------- 1 | @media print { 2 | display: none; 3 | } 4 | 5 | @media screen and (max-width: 500px) and (min-width: 100px), (max-width: 500px) and (min-height: 200px) { 6 | .foo { 7 | color: #100; 8 | } 9 | } 10 | 11 | .foo { 12 | @for $i from 1 through 4 { 13 | @media (min-width: 2 * ($i + 7) px) { 14 | width: 100px * $i; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/stylus/if.styl: -------------------------------------------------------------------------------- 1 | a = 10 2 | b = 5 3 | 4 | colorTheme() 5 | 'red' 6 | 7 | div 8 | if a > b 9 | color: red 10 | else if a < b 11 | color: yellow 12 | else 13 | color: blue 14 | 15 | .test-conditionals 16 | unless colorTheme() is 'red' 17 | color: red 18 | if colorTheme() is 'blue' { 19 | color: red 20 | } else { 21 | color: blue 22 | } 23 | -------------------------------------------------------------------------------- /test/vue/stylus/multiple-style-blocks.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 13 | 14 | 18 | 19 | 24 | -------------------------------------------------------------------------------- /test/stylus/keyframes.styl: -------------------------------------------------------------------------------- 1 | keyframe-name = pulse 2 | $val = 20 3 | 4 | @keyframes { $keyframe-name } 5 | for i in 0..5 6 | {20% * i} 7 | opacity (i / $val) 8 | 9 | @keyframes auto-color 10 | 0% 11 | color red 12 | 50% 13 | color blue 14 | 100% 15 | color yellow 16 | 17 | @keyframes foo 18 | from 19 | color: black 20 | to 21 | color: white 22 | -------------------------------------------------------------------------------- /test/test.styl: -------------------------------------------------------------------------------- 1 | default-border-radius(prop, args) 2 | -webkit-{prop}-radius args 3 | -moz-{prop}-radius args 4 | {prop}-radius args 5 | 6 | block_mixin(myColor) 7 | color: myColor 8 | .prop 9 | font-size: 14px; 10 | 11 | .child 12 | { block } 13 | 14 | body 15 | default-border-radius(border, 4px) 16 | +block_mixin(blue) 17 | margin: 10px 18 | padding: 10px 19 | -------------------------------------------------------------------------------- /test/vue/scss/multiple-style-blocks.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 14 | 15 | 20 | 21 | 26 | -------------------------------------------------------------------------------- /test/stylus/mixins.styl: -------------------------------------------------------------------------------- 1 | default-border-radius(prop, args) 2 | -webkit-{prop}-radius args 3 | -moz-{prop}-radius args 4 | {prop}-radius args 5 | 6 | block_mixin(myColor) 7 | color: myColor 8 | .prop 9 | font-size: 14px; 10 | 11 | .child 12 | { block } 13 | 14 | body 15 | default-border-radius(border, 4px) 16 | +block_mixin(blue) 17 | margin: 10px 18 | padding: 10px -------------------------------------------------------------------------------- /test/stylus/object.styl: -------------------------------------------------------------------------------- 1 | palette = { 2 | orange: { 3 | light: lighten(#f89c48, 10%), 4 | base: #f89c48, 5 | dark: darken(#f89c48, 10%) 6 | }, 7 | green: { 8 | light: lighten(#28ba00, 10%), 9 | base: #28ba00, 10 | dark: darken(#28ba00, 10%) 11 | } 12 | } 13 | 14 | .selector 15 | background-color: palette.orange.base 16 | border-color: palette.orange.dark 17 | color: palette.green.base 18 | -------------------------------------------------------------------------------- /test/scss/if.scss: -------------------------------------------------------------------------------- 1 | $a: 10; 2 | $b: 5; 3 | 4 | @function colorTheme() { 5 | @return 'red' 6 | } 7 | 8 | div { 9 | @if $a > $b { 10 | color: red; 11 | } @else if $a < $b { 12 | color: yellow; 13 | } @else { 14 | color: blue; 15 | } 16 | } 17 | 18 | .test-conditionals { 19 | @if colorTheme() != 'red' { 20 | color: red; 21 | } 22 | @if colorTheme() == 'blue' { 23 | color: red; 24 | } @else { 25 | color: blue; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/scss/variable-search.scss: -------------------------------------------------------------------------------- 1 | #logo { 2 | $w: 150px; 3 | width: $w; 4 | $h: 80px; 5 | height: $h; 6 | margin-left: -($w / 2); 7 | margin-top: -($h / 2); 8 | 9 | /* property lookup */ 10 | div { 11 | width: $w; 12 | } 13 | } 14 | 15 | body { 16 | color: red; 17 | ul { 18 | li { 19 | color: blue; 20 | a { 21 | /* property up bubble lookup */ 22 | background-color: blue; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/scss/keyframes.scss: -------------------------------------------------------------------------------- 1 | $keyframe-name: pulse; 2 | $val: 20; 3 | 4 | @keyframes #{$keyframe-name} { 5 | @for $i from 0 through 5 { 6 | #{20% * $i} { 7 | opacity: $i / $val; 8 | } 9 | } 10 | } 11 | 12 | @keyframes auto-color { 13 | 0% { 14 | color: red; 15 | } 16 | 50% { 17 | color: blue; 18 | } 19 | 100% { 20 | color: yellow; 21 | } 22 | } 23 | 24 | @keyframes foo { 25 | from { 26 | color: black; 27 | } 28 | to { 29 | color: white; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/test.scss: -------------------------------------------------------------------------------- 1 | @mixin default-border-radius($prop, $args) { 2 | -webkit-#{$prop}-radius: $args; 3 | -moz-#{$prop}-radius: $args; 4 | #{$prop}-radius: $args; 5 | } 6 | 7 | @mixin block_mixin($myColor) { 8 | color: $myColor; 9 | .prop { 10 | font-size: 14px; 11 | 12 | .child { 13 | @content; 14 | }; 15 | }; 16 | } 17 | 18 | body { 19 | @include default-border-radius(border, 4px); 20 | @include block_mixin(blue) { 21 | margin: 10px; 22 | padding: 10px; 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /test/scss/mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin default-border-radius($prop, $args) { 2 | -webkit-#{$prop}-radius: $args; 3 | -moz-#{$prop}-radius: $args; 4 | #{$prop}-radius: $args; 5 | } 6 | 7 | @mixin block_mixin($myColor) { 8 | color: $myColor; 9 | .prop { 10 | font-size: 14px; 11 | 12 | .child { 13 | @content; 14 | } 15 | } 16 | } 17 | 18 | body { 19 | @include default-border-radius(border, 4px); 20 | @include block_mixin(blue) { 21 | margin: 10px; 22 | padding: 10px; 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /test/scss/object.scss: -------------------------------------------------------------------------------- 1 | $palette: ( 2 | 'orange': ( 3 | 'light': lighten(#f89c48, 10%), 4 | 'base': #f89c48, 5 | 'dark': darken(#f89c48, 10%) 6 | ), 7 | 'green': ( 8 | 'light': lighten(#28ba00, 10%), 9 | 'base': #28ba00, 10 | 'dark': darken(#28ba00, 10%) 11 | ) 12 | ); 13 | 14 | .selector { 15 | background-color: map-get(map-get($palette, 'orange'), 'base'); 16 | border-color: map-get(map-get($palette, 'orange'), 'dark'); 17 | color: map-get(map-get($palette, 'green'), 'base'); 18 | } 19 | -------------------------------------------------------------------------------- /test/stylus/functions.styl: -------------------------------------------------------------------------------- 1 | $width = calc(100vw - 32px) 2 | div 3 | box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.2), 4 | 0 1px 1px 0 rgba(0, 0, 0, 0.14), 5 | 0 2px 1px -1px rgba(0, 0, 0, 0.12) 6 | 7 | div 8 | animation: name_of_animation 0.8s cubic-bezier(0.36, 0.07, 0.19, 0.97) both 9 | background: 10 | linear-gradient(white 30%, hsla(0, 0%, 100%, 0)), 11 | linear-gradient(hsla(0, 0%, 100%, 0) 10px, white 70%) bottom, 12 | radial-gradient(at top, rgba(0, 0, 0, 0.2), transparent 70%), 13 | radial-gradient(at bottom, rgba(0, 0, 0, 0.2), transparent 70%) bottom 14 | -------------------------------------------------------------------------------- /test/scss/functions.scss: -------------------------------------------------------------------------------- 1 | $width: calc(100vw - 32px); 2 | div { 3 | box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.2), 4 | 0 1px 1px 0 rgba(0, 0, 0, 0.14), 5 | 0 2px 1px -1px rgba(0, 0, 0, 0.12); 6 | } 7 | 8 | div { 9 | animation: name_of_animation 0.8s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; 10 | background: linear-gradient(white 30%, hsla(0, 0%, 100%, 0)), 11 | linear-gradient(hsla(0, 0%, 100%, 0) 10px, white 70%) bottom, 12 | radial-gradient(at top, rgba(0, 0, 0, 0.2), transparent 70%), 13 | radial-gradient(at bottom, rgba(0, 0, 0, 0.2), transparent 70%) bottom; 14 | } 15 | -------------------------------------------------------------------------------- /bin/convertVueFile.js: -------------------------------------------------------------------------------- 1 | const { converter } = require('../lib') 2 | 3 | function convertVueFile(vueTemplate, options) { 4 | let newVueTemplate = vueTemplate; 5 | const styleRegEx = /([\w\W]*?)<\/style>/g; 6 | let match; 7 | while ((match = styleRegEx.exec(newVueTemplate)) !== null) { 8 | if (match[1].includes('stylus')) { 9 | let style = match[2] || ''; 10 | if (style.trim()) { 11 | style = converter(style, options); 12 | } 13 | const isScoped = match[1].includes('scoped'); 14 | const styleText = ``; 15 | newVueTemplate = newVueTemplate.replace(match[0], styleText); 16 | } 17 | } 18 | return newVueTemplate; 19 | } 20 | 21 | module.exports = convertVueFile; 22 | -------------------------------------------------------------------------------- /test/stylus/comment.styl: -------------------------------------------------------------------------------- 1 | /* http://stylus-lang.com/docs/comments.html */ 2 | 3 | // Single-line comments look like JavaScript comments, and do not output in the resulting CSS: 4 | 5 | // Same goes for multiple single-line comments 6 | // adjacent to each other 7 | // on separate lines 8 | // http://stylus-lang.com/docs/comments.html 9 | 10 | /* 11 | * Multi-line comments look identical to regular CSS comments. 12 | * However, they only output when the compress option is not enabled. 13 | */ 14 | body 15 | padding 5px 16 | 17 | /*! 18 | * Multi-line comments which are not suppressed start with /*!. 19 | * This tells Stylus to output the comment regardless of compression. 20 | */ 21 | add(a, b) 22 | a + b 23 | 24 | // sign comment 25 | 26 | div 27 | color: red // inline comment 28 | font-size: 16px /* inline comment */ 29 | 30 | $font-size = 16px // variable inline comment 31 | -------------------------------------------------------------------------------- /test/scss/comment.scss: -------------------------------------------------------------------------------- 1 | /* http://stylus-lang.com/docs/comments.html */ 2 | 3 | // Single-line comments look like JavaScript comments, and do not output in the resulting CSS: 4 | 5 | // Same goes for multiple single-line comments 6 | // adjacent to each other 7 | // on separate lines 8 | // http://stylus-lang.com/docs/comments.html 9 | 10 | /* 11 | * Multi-line comments look identical to regular CSS comments. 12 | * However, they only output when the compress option is not enabled. 13 | */ 14 | body { 15 | padding: 5px; 16 | } 17 | 18 | /*! 19 | * Multi-line comments which are not suppressed start with /*!. 20 | * This tells Stylus to output the comment regardless of compression. 21 | */ 22 | @function add($a, $b) { 23 | @return $a + $b 24 | } 25 | 26 | // sign comment 27 | 28 | div { 29 | color: red; // inline comment 30 | font-size: 16px; /* inline comment */ 31 | } 32 | 33 | $font-size: 16px; // variable inline comment 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 txs1992 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bin/findMixin.js: -------------------------------------------------------------------------------- 1 | const { nodeToJSON, _get } = require('../lib') 2 | 3 | const MIXIN_TYPES = [ 4 | 'Selector', 5 | 'Property', 6 | ] 7 | 8 | function isCallMixin(node) { 9 | return node.__type === 'Call' && node.block 10 | } 11 | 12 | function findMixin(node, mixins = [], fnList = []) { 13 | // val =》 obj, block -> obj, nodes -> arr 14 | if (node.__type === 'Function' && fnList.indexOf(node.name) < 0) { 15 | fnList.push(node.name) 16 | } 17 | if (fnList.length &&(MIXIN_TYPES.indexOf(node.__type) > -1 || isCallMixin(node))) { 18 | fnList.forEach(name => { 19 | if (mixins.indexOf(name) < 0) { 20 | mixins.push(name) 21 | } 22 | }) 23 | fnList = [] 24 | } 25 | if (_get(node, ['val', 'toJSON'])) { 26 | findMixin(node.val.toJSON(), mixins, fnList) 27 | } 28 | if (_get(node, ['expr', 'toJSON'])) { 29 | findMixin(node.expr.toJSON(), mixins, fnList) 30 | } 31 | if (_get(node, ['block', 'toJSON'])) { 32 | findMixin(node.block.toJSON(), mixins, fnList) 33 | } 34 | if (node.nodes) { 35 | const nodes = nodeToJSON(node.nodes) 36 | nodes.forEach(item => findMixin(item, mixins, fnList)) 37 | } 38 | return mixins 39 | } 40 | 41 | module.exports = findMixin; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Parser from 'stylus/lib/parser.js' 2 | import visitor from './visitor/index.js' 3 | import { _get as get, nodesToJSON } from './util.js' 4 | 5 | export function parse(result) { 6 | return new Parser(result).parse() 7 | } 8 | 9 | export function nodeToJSON(data) { 10 | return nodesToJSON(data) 11 | } 12 | 13 | export function _get(obj, pathArray, defaultValue) { 14 | return get(obj, pathArray, defaultValue) 15 | } 16 | 17 | export function converter(result, options = { 18 | quote: `'`, 19 | conver: 'sass', 20 | autoprefixer: true 21 | }, globalVariableList = [], globalMixinList = []) { 22 | if (options.isSignComment) result = result.replace(/\/\/\s(.*)/g, '/* !#sign#! $1 */') 23 | 24 | // Add semicolons to properties with inline comments to ensure that they are parsed correctly 25 | result = result.replace(/^( *)(\S(.+?))( *)(\/\*.*\*\/)$/gm, '$1$2;$4$5'); 26 | 27 | if (typeof result !== 'string') return result 28 | const ast = new Parser(result).parse() 29 | // 开发时查看 ast 对象。 30 | // console.log(JSON.stringify(ast)) 31 | const text = visitor(ast, options, globalVariableList, globalMixinList) 32 | // Convert special multiline comments to single-line comments 33 | return text.replace(/\/\*\s!#sign#!\s(.*)\s\*\//g, '// $1') 34 | } 35 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | export function repeatString(str, num) { 2 | return num > 0 ? str.repeat(num) : '' 3 | } 4 | 5 | export function nodesToJSON(nodes) { 6 | return nodes.map(node => 7 | Object.assign( 8 | { 9 | // default in case not in node 10 | nodes: [] 11 | }, 12 | node.toJSON() 13 | ) 14 | ) 15 | } 16 | 17 | export function trimEdeg(str) { 18 | return str.replace(/(^\s*)|(\s*$)/g, '') 19 | } 20 | 21 | export function trimFirst(str) { 22 | return str.replace(/(^\s*)/g, '') 23 | } 24 | 25 | export function tirmFirstLength(str) { 26 | return str.length - trimFirst(str).length 27 | } 28 | 29 | export function trimLinefeed(str) { 30 | return str.replace(/^\n*/, '') 31 | } 32 | 33 | export function trimFirstLinefeedLength(str) { 34 | return tirmFirstLength(trimLinefeed(str)) 35 | } 36 | 37 | export function replaceFirstATSymbol(str, temp = '$') { 38 | return str.replace(/^\$|/, temp) 39 | } 40 | 41 | export function getCharLength(str, char) { 42 | return str.split(char).length - 1 43 | } 44 | 45 | export function _get(obj, pathArray, defaultValue) { 46 | if (obj == null) return defaultValue 47 | 48 | let value = obj 49 | 50 | pathArray = [].concat(pathArray) 51 | 52 | for (let i = 0; i < pathArray.length; i += 1) { 53 | const key = pathArray[i] 54 | value = value[key] 55 | if (value == null) { 56 | return defaultValue 57 | } 58 | } 59 | 60 | return value 61 | } 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stylus-converter", 3 | "version": "0.8.1", 4 | "description": "A tool that converts a stylus into sass, or less, or other precompiled CSS.", 5 | "main": "lib/index.js", 6 | "bin": { 7 | "stylus-conver": "bin/conver.js" 8 | }, 9 | "scripts": { 10 | "build": "rollup -c", 11 | "dev": "rollup -c && node ./test/converter-test.js", 12 | "test": "npm run build && mocha ./test/index.spec.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/txs1992/stylus-converter.git" 17 | }, 18 | "keywords": [ 19 | "stylus", 20 | "stylus-converter", 21 | "stylus-to", 22 | "stylus2", 23 | "stylusto", 24 | "stylsu2scss", 25 | "stylus2sass", 26 | "stylus2less", 27 | "stylusToScss", 28 | "stylusToSass", 29 | "stylusToLess", 30 | "stylustoscss", 31 | "stylustosass", 32 | "stylustoless", 33 | "stylus-to-scss", 34 | "stylus-to-sass", 35 | "stylus-to-lcss" 36 | ], 37 | "author": "MT", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/txs1992/stylus-converter/issues" 41 | }, 42 | "homepage": "https://github.com/txs1992/stylus-converter#readme", 43 | "devDependencies": { 44 | "babel": "^6.23.0", 45 | "babel-core": "^6.26.3", 46 | "babel-plugin-external-helpers": "^6.22.0", 47 | "babel-preset-env": "^1.7.0", 48 | "babel-preset-es2015-rollup": "^3.0.0", 49 | "chai": "^4.1.2", 50 | "mocha": "^5.1.1", 51 | "rollup": "^0.57.1", 52 | "rollup-plugin-babel": "^3.0.3" 53 | }, 54 | "dependencies": { 55 | "commander": "^2.15.1", 56 | "invariant": "^2.2.4", 57 | "lodash.debounce": "^4.0.8", 58 | "lodash.uniq": "^4.5.0", 59 | "ms": "^2.1.1", 60 | "optimist": "^0.6.1", 61 | "ora": "^2.1.0", 62 | "stylus": "^0.54.5" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /bin/conver.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const ora = require('ora') 4 | const argv = require('optimist').argv 5 | const program = require('commander') 6 | const converFile = require('./file') 7 | const version = require('../package.json').version 8 | 9 | const spinner = ora({ 10 | color: 'yellow' 11 | }) 12 | 13 | function handleOptions() { 14 | const quote = argv.q || argv.quote || 'single' 15 | const input = argv.i || argv.input 16 | const output = argv.o || argv.output 17 | const conver = argv.c || argv.conver || 'scss' 18 | const directory = argv.d || argv.directory || 'no' 19 | const autoprefixer = argv.p || argv.autoprefixer || 'yes' 20 | const isSignComment = argv.s || argv.singlecomments || 'no' 21 | const indentVueStyleBlock = argv.v || argv.indentVueStyleBlock || 0 22 | if (!input) throw new Error('The input parameter cannot be empty.') 23 | if (!output) throw new Error('The output parameter cannot be empty.') 24 | if (quote !== 'single' && quote !== 'dobule') throw new Error('The quote parameter has a problem, it can only be single or double.') 25 | if (conver.toLowerCase() !== 'scss') throw new Error('The conver parameter can only be scss.') 26 | 27 | spinner.start('Your file is being converted. Please wait...\n') 28 | converFile({ 29 | quote: quote === 'single' ? '\'' : '\"', 30 | input, 31 | output, 32 | conver, 33 | directory: directory === 'yes', 34 | autoprefixer: autoprefixer === 'yes', 35 | isSignComment: isSignComment === 'yes', 36 | indentVueStyleBlock: Number(indentVueStyleBlock), 37 | }, time => { 38 | console.log('') 39 | spinner.succeed('Conversion completed and time spent ' + time + ' ms.') 40 | }) 41 | } 42 | 43 | program 44 | .version(version) 45 | .option('-q, --quote', 'Add quote') 46 | .option('-i, --input', 'Add input') 47 | .option('-o, --output', 'Add output') 48 | .option('-c, --conver', 'Add conver type') 49 | .option('-d, --directory', 'Is directory type') 50 | .option('-p, --autoprefixer', 'Whether to add a prefix') 51 | .option('-s, --singlecomments ', 'Change single-line comments to multi-line comments') 52 | .option('-v, --indentVueStyleBlock ', 'Indent the entire style block of a vue file with a certain amount of spaces.') 53 | .action(handleOptions) 54 | .parse(process.argv); 55 | -------------------------------------------------------------------------------- /bin/convertStylus.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const { parse, converter, nodeToJSON } = require('../lib') 3 | const findMixin = require('./findMixin') 4 | const convertVueFile = require('./convertVueFile') 5 | let callLen = 0 6 | const GLOBAL_MIXIN_NAME_LIST = [] 7 | const GLOBAL_VARIABLE_NAME_LIST = [] 8 | 9 | function convertStylus(input, output, options, callback) { 10 | callLen++ 11 | if (/\.styl$/.test(input) || /\.vue$/.test(input)) { 12 | fs.readFile(input, (err, res) => { 13 | if (err) throw err 14 | let result = res.toString() 15 | let outputPath = output 16 | if (/\.styl$/.test(input)) { 17 | try { 18 | if (options.status === 'complete') { 19 | result = converter(result, options, GLOBAL_VARIABLE_NAME_LIST, GLOBAL_MIXIN_NAME_LIST) 20 | } else { 21 | const ast = parse(result) 22 | const nodes = nodeToJSON(ast.nodes) 23 | nodes.forEach(node => { 24 | findMixin(node, GLOBAL_MIXIN_NAME_LIST) 25 | if (node.__type === 'Ident' && node.val.toJSON().__type === 'Expression') { 26 | if (GLOBAL_VARIABLE_NAME_LIST.indexOf(node.name) === -1) { 27 | GLOBAL_VARIABLE_NAME_LIST.push(node.name) 28 | } 29 | } 30 | }) 31 | } 32 | } catch (e) { 33 | result = '' 34 | callLen-- 35 | console.error('Failed to convert', input) 36 | return; 37 | } 38 | outputPath = output.replace(/\.styl$/, '.' + options.conver) 39 | } else { 40 | //处理 vue 文件 41 | result = convertVueFile(result, options); 42 | } 43 | fs.writeFile(outputPath, result, err => { 44 | callLen-- 45 | if (err) throw err 46 | if (!result) return 47 | if (callLen === 0) { 48 | if (options.status === 'complete') { 49 | callback(Date.now()) 50 | } else { 51 | callback() 52 | } 53 | } 54 | }) 55 | }) 56 | } else { 57 | fs.copyFile(input, output, err => { 58 | callLen-- 59 | if (err) throw err 60 | if (options.status !== 'complete') return 61 | if (callLen === 0) { 62 | if (options.status === 'complete') { 63 | callback(Date.now()) 64 | } else { 65 | callback() 66 | } 67 | } 68 | }) 69 | } 70 | } 71 | 72 | module.exports = convertStylus -------------------------------------------------------------------------------- /test/scss/autoprefixer-keyframes.scss: -------------------------------------------------------------------------------- 1 | $keyframe-name: pulse; 2 | $val: 20; 3 | 4 | @-webkit-keyframes #{$keyframe-name} { 5 | @for $i from 0 through 5 { 6 | #{20% * $i} { 7 | opacity: $i / $val; 8 | } 9 | } 10 | } 11 | 12 | @-moz-keyframes #{$keyframe-name} { 13 | @for $i from 0 through 5 { 14 | #{20% * $i} { 15 | opacity: $i / $val; 16 | } 17 | } 18 | } 19 | 20 | @-ms-keyframes #{$keyframe-name} { 21 | @for $i from 0 through 5 { 22 | #{20% * $i} { 23 | opacity: $i / $val; 24 | } 25 | } 26 | } 27 | 28 | @-o-keyframes #{$keyframe-name} { 29 | @for $i from 0 through 5 { 30 | #{20% * $i} { 31 | opacity: $i / $val; 32 | } 33 | } 34 | } 35 | 36 | @keyframes #{$keyframe-name} { 37 | @for $i from 0 through 5 { 38 | #{20% * $i} { 39 | opacity: $i / $val; 40 | } 41 | } 42 | } 43 | 44 | @-webkit-keyframes auto-color { 45 | 0% { 46 | color: red; 47 | } 48 | 50% { 49 | color: blue; 50 | } 51 | 100% { 52 | color: yellow; 53 | } 54 | } 55 | 56 | @-moz-keyframes auto-color { 57 | 0% { 58 | color: red; 59 | } 60 | 50% { 61 | color: blue; 62 | } 63 | 100% { 64 | color: yellow; 65 | } 66 | } 67 | 68 | @-ms-keyframes auto-color { 69 | 0% { 70 | color: red; 71 | } 72 | 50% { 73 | color: blue; 74 | } 75 | 100% { 76 | color: yellow; 77 | } 78 | } 79 | 80 | @-o-keyframes auto-color { 81 | 0% { 82 | color: red; 83 | } 84 | 50% { 85 | color: blue; 86 | } 87 | 100% { 88 | color: yellow; 89 | } 90 | } 91 | 92 | @keyframes auto-color { 93 | 0% { 94 | color: red; 95 | } 96 | 50% { 97 | color: blue; 98 | } 99 | 100% { 100 | color: yellow; 101 | } 102 | } 103 | 104 | @-webkit-keyframes foo { 105 | from { 106 | color: black; 107 | } 108 | to { 109 | color: white; 110 | } 111 | } 112 | 113 | @-moz-keyframes foo { 114 | from { 115 | color: black; 116 | } 117 | to { 118 | color: white; 119 | } 120 | } 121 | 122 | @-ms-keyframes foo { 123 | from { 124 | color: black; 125 | } 126 | to { 127 | color: white; 128 | } 129 | } 130 | 131 | @-o-keyframes foo { 132 | from { 133 | color: black; 134 | } 135 | to { 136 | color: white; 137 | } 138 | } 139 | 140 | @keyframes foo { 141 | from { 142 | color: black; 143 | } 144 | to { 145 | color: white; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /bin/file.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const debounce = require('lodash.debounce') 3 | const convertStylus = require('./convertStylus') 4 | 5 | let startTime = 0 6 | 7 | function getStat(path, callback) { 8 | fs.stat(path, (err, stats) => { 9 | if (err) throw err 10 | callback(stats) 11 | }) 12 | } 13 | 14 | function readDir(path, callback, errorHandler) { 15 | fs.readdir(path, (err, files) => { 16 | if (err) { 17 | errorHandler() 18 | } else { 19 | callback(files) 20 | } 21 | }) 22 | } 23 | 24 | function mkDir(path, callback) { 25 | fs.mkdir(path, err => { 26 | if (err) throw err 27 | callback() 28 | }) 29 | } 30 | 31 | function readAndMkDir(input, output, callback) { 32 | readDir(output, () => { 33 | readDir(input, callback) 34 | }, () => { 35 | mkDir(output, () => { 36 | readDir(input, callback) 37 | }) 38 | }) 39 | } 40 | 41 | function visitDirectory(input, output, inputParent, outputParent, options, callback) { 42 | const inputPath = inputParent ? inputParent + input : input 43 | const outputPath = outputParent ? outputParent + output : output 44 | getStat(inputPath, stats => { 45 | if (stats.isFile()) { 46 | convertStylus(inputPath, outputPath, options, callback) 47 | } else if (stats.isDirectory()) { 48 | readAndMkDir(inputPath, outputPath, files => { 49 | files.forEach(file => { 50 | if (inputParent) { 51 | visitDirectory(file, file, inputPath + '/', outputPath + '/', options, callback) 52 | } else { 53 | visitDirectory(file, file, input + '/', output + '/', options, callback) 54 | } 55 | }) 56 | }) 57 | } 58 | }) 59 | } 60 | 61 | function handleStylus(options, callback) { 62 | const input = options.input 63 | const output = options.output 64 | if (options.directory) { 65 | const baseInput = /\/$/.test(options.input) 66 | ? input.substring(0, input.length - 1) 67 | : input 68 | const baseOutput = /\/$/.test(options.output) 69 | ? output.substring(0, output.length - 1) 70 | : output 71 | visitDirectory(baseInput, baseOutput, '', '', options, callback) 72 | } else { 73 | convertStylus(input, output, options, callback) 74 | } 75 | } 76 | 77 | const handleCall = debounce(function (now, startTime, callback) { 78 | callback(now - startTime) 79 | }, 500) 80 | 81 | function converFile(options, callback) { 82 | startTime = Date.now() 83 | options.status = 'ready' 84 | handleStylus(options, () => { 85 | options.status = 'complete' 86 | handleStylus(options, now => { 87 | // handleCall(now, startTime, callback) 88 | callback(now - startTime) 89 | }) 90 | }) 91 | } 92 | 93 | module.exports = converFile; 94 | -------------------------------------------------------------------------------- /doc/zh-cn.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |

25 | 26 |
27 |

28 | 29 | 中文 30 | 31 | | 32 | 33 | English 34 | 35 |

36 |
37 | 38 | ## 为什么要做这个工具 39 | 40 | > 因为早期有个项目用到了 stylus,stylus 开发起来很爽,但 stylus 基于缩进的代码在修改的时候不是很方便,加上所在团队开发使用的都是 SCSS ,为了便于维护和统一,准备将项目中的 stylus 替换成 SCSS。手动转换 stylus 浪费时间,且出错率大,当时在想也许别人也有这样的需求呢,所以就做了这样一个项目。**请各位大佬动动你们发财的小手,给我点个 `star`,不胜感激。^_^** 41 | 42 | ## stylus-converter 配置 43 | 44 | ### converter 配置 45 | 46 | | 参数 | 说明 | 类型 | 可选值 | 默认值 | 47 | | ---- | ---- | ---- | ---- | ---- | 48 | | `quote` | 转换中遇到字符串时,使用的引号类型 | string | `'` / `"` | `'` | 49 | | `conver` | 转换类型,例如转换成 scss 语法 | string | scss | scss | 50 | | `autoprefixer` | 是否自动添加前缀,stylus 在转换 css 语法的时候,有些语法会自动添加前缀例如 `@keyframes` | boolean | true / false | true | 51 | | `indentVueStyleBlock` | 在 `.vue` 文件中转换 stylus 时,可以添加一定数量的缩进,默认不添加缩进。 | number | number | 0 | 52 | 53 | ### cli 配置 54 | 55 | | 参数 | 简写 | 说明 | 可选值 | 默认值 | 56 | | ---- | ---- | ---- | ---- | ---- | 57 | | `--quote` | `-q` | 转换中遇到字符串时,使用的引号类型 | single / dobule | single | 58 | | `--input` | `-i` | 输入名称,可以是文件或者是文件夹的路径 | - | - | 59 | | `--output` | `-o` | 输出名称,可以是文件或者是文件夹的路径 | - | - | 60 | | `--conver ` | `-c` | 转换类型,例如转换成 scss 语法 | scss | scss | 61 | | `--directory` | `-d` | 输入和输出路径是否是个目录 | yes / no | no | 62 | | `--autoprefixer` | `-p` | 是否添加前缀 | yes / no | yes | 63 | | `--indentVueStyleBlock` | `-v` | 在 `.vue` 文件中转换 stylus 时,可以添加一定数量的缩进,默认不添加缩进。 | number | 0 | 64 | 65 | ### 如何处理单行注释。 66 | ```js 67 | 1. 先 fork 项目再 clone 项目到本地 68 | git clone git@github.com:/stylus-converter.git 69 | 70 | 2. 进入项目目录 71 | cd stylus-converter 72 | 73 | 3. 安装项目依赖 74 | npm install 75 | 76 | 4. 进入 `node_modules/stylus/lib/lexer.js` 文件第 581 行。 77 | 78 | 5. 修改下列代码。 79 | // 修改前 80 | if ('/' == this.str[0] && '/' == this.str[1]) { 81 | var end = this.str.indexOf('\n'); 82 | if (-1 == end) end = this.str.length; 83 | this.skip(end); 84 | return this.advance(); 85 | } 86 | 87 | // 修改后 88 | if ('/' == this.str[0] && '/' == this.str[1]) { 89 | var end = this.str.indexOf('\n'); 90 | const str = this.str.substring(0, end) 91 | if (-1 == end) end = this.str.length; 92 | this.skip(end); 93 | return new Token('comment', new nodes.Comment(str, suppress, true)) 94 | } 95 | ``` 96 | 97 | ## 使用示例 98 | 99 | ```javascript 100 | // 下载 stylus-converter 101 | npm install -g stylus-converter 102 | 103 | // 运行 cli 转换文件 104 | stylus-conver -i test.styl -o test.scss 105 | 106 | // 运行 cli 转换目录 107 | // 先进入项目目录 108 | mv src src-temp 109 | stylus-conver -d yes -i src-temp -o src 110 | ``` 111 | 112 | ## 转换文件比较 113 | 114 | ### 转换前的 stylus 源码 115 | 116 | ```stylus 117 | handleParams(args...) 118 | args 119 | 120 | @media screen and (max-width: 500px) and (min-width: 100px), (max-width: 500px) and (min-height: 200px) 121 | .foo 122 | color: #100 123 | 124 | .foo 125 | for i in 1..4 126 | @media (min-width: 2 * (i + 7) px) 127 | ``` 128 | 129 | ### 转换后的 SCSS 源码 130 | 131 | ```scss 132 | @function handleParams($args...) { 133 | @return $args; 134 | } 135 | 136 | @media screen and (max-width: 500px) and (min-width: 100px), (max-width: 500px) and (min-height: 200px) { 137 | .foo { 138 | color: #100; 139 | } 140 | } 141 | 142 | .foo { 143 | @for $i from 1 through 4 { 144 | @media (min-width: 2 * ($i + 7) px) { 145 | width: 100px * $i; 146 | } 147 | } 148 | } 149 | ``` 150 | 151 | > 如果你不想你转换的 @keyframes 添加默认前缀,请设置 `options.autoprefixer = false` 152 | 153 | ### 转换前的 `.vue` 文件 154 | ```html 155 | 160 | 161 | 166 | ``` 167 | 168 | ### 转换后的 `.vue` 文件 169 | ```html 170 | 175 | 176 | 183 | ``` 184 | 185 | ## 搭建开发环境 186 | 187 | ```text 188 | 1. 先 fork 项目再 clone 项目到本地 189 | git clone git@github.com:/stylus-converter.git 190 | 191 | 2. 进入项目目录 192 | cd stylus-converter 193 | 194 | 3. 安装项目依赖 195 | npm install 196 | 197 | 4. 打包编译源文件 198 | npm run build 199 | 200 | 5. 本地调试 cli 201 | npm link 202 | ``` 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |

25 | 26 |
27 |

28 | 29 | 中文 30 | 31 | | 32 | 33 | English 34 | 35 |

36 |
37 | 38 | ## What is this 39 | 40 | > A tool that converts a stylus into scss, or less, or other precompiled CSS. 41 | 42 | ## stylus-converter config 43 | 44 | ### converter options 45 | 46 | | Attribute | Description | Type | Accepted Values | Default | 47 | | ---- | ---- | ---- | ---- | ---- | 48 | | `quote` | The quote type to use when converting strings | string | `'` / `"` | `'` | 49 | | `conver` | Conversion type, such as conversion to scss syntax | string | scss | scss | 50 | | `autoprefixer` | Whether or not to automatically add a prefix, stylus will automatically add prefixes when converting stylus grammars. `@keyframes` | boolean | true / false | true | 51 | | `indentVueStyleBlock` | Indent the entire style block of a vue file with a certain amount of spaces. | number | number | 0 | 52 | 53 | ### cli options 54 | 55 | | Attribute | Shorthand | Description | Accepted Values | Default | 56 | | ---- | ---- | ---- | ---- | ---- | 57 | | `--quote` | `-q` | The quote type to use when converting strings | single / dobule | single | 58 | | `--input` | `-i` | Enter a name, which can be a path to a file or a folder | - | - | 59 | | `--output` | `-o` | Output name, can be a path to a file or a folder | - | - | 60 | | `--conver ` | `-c` | Conversion type, such as conversion to scss syntax | scss | scss | 61 | | `--directory` | `-d` | Whether the input and output paths are directories | yes / no | no | 62 | | `--autoprefixer` | `-p` | Whether to add a prefix | yes / no | yes | 63 | | `--indentVueStyleBlock` | `-v` | Indent the entire style block of a vue file with a certain amount of spaces. | number | 0 | 64 | 65 | ### How to handle single line comments 66 | ```js 67 | 1. First fork project and then clone project to local 68 | git clone git@github.com:/stylus-converter.git 69 | 70 | 2. Enter the project directory 71 | cd stylus-converter 72 | 73 | 3. Installation project depends 74 | npm install 75 | 76 | 4. Go to line 581 of the `node_modules/stylus/lib/lexer.js` file. 77 | 78 | 5. Modify the code below. 79 | // before modification 80 | if ('/' == this.str[0] && '/' == this.str[1]) { 81 | var end = this.str.indexOf('\n'); 82 | if (-1 == end) end = this.str.length; 83 | this.skip(end); 84 | return this.advance(); 85 | } 86 | 87 | // After modification 88 | if ('/' == this.str[0] && '/' == this.str[1]) { 89 | var end = this.str.indexOf('\n'); 90 | const str = this.str.substring(0, end) 91 | if (-1 == end) end = this.str.length; 92 | this.skip(end); 93 | return new Token('comment', new nodes.Comment(str, suppress, true)) 94 | } 95 | ``` 96 | 97 | ## Use examples 98 | 99 | ```javascript 100 | // download stylus-converter 101 | npm install -g stylus-converter 102 | 103 | // Run the cli conversion file 104 | stylus-conver -i test.styl -o test.scss 105 | 106 | // Run the cli conversion directory 107 | // cd your project 108 | mv src src-temp 109 | stylus-conver -d yes -i src-temp -o src 110 | ``` 111 | 112 | ## Conversion file comparison 113 | 114 | ### Stylus source code before conversion 115 | 116 | ```stylus 117 | handleParams(args...) 118 | args 119 | 120 | @media screen and (max-width: 500px) and (min-width: 100px), (max-width: 500px) and (min-height: 200px) 121 | .foo 122 | color: #100 123 | 124 | .foo 125 | for i in 1..4 126 | @media (min-width: 2 * (i + 7) px) 127 | ``` 128 | 129 | ### Converted SCSS source code 130 | 131 | ```scss 132 | @function handleParams($args...) { 133 | @return $args; 134 | } 135 | 136 | @media screen and (max-width: 500px) and (min-width: 100px), (max-width: 500px) and (min-height: 200px) { 137 | .foo { 138 | color: #100; 139 | } 140 | } 141 | 142 | .foo { 143 | @for $i from 1 through 4 { 144 | @media (min-width: 2 * ($i + 7) px) { 145 | width: 100px * $i; 146 | } 147 | } 148 | } 149 | ``` 150 | 151 | > If you do not want to add the default prefix for your converted @keyframes, please set `options.autoprefixer = false` 152 | 153 | ### The `.vue` file before conversion 154 | 155 | ```html 156 | 161 | 162 | 167 | ``` 168 | 169 | ### Converted `.vue` file 170 | 171 | ```html 172 | 177 | 178 | 185 | ``` 186 | 187 | ## Build a development environment 188 | 189 | ```text 190 | 1. First fork project and then clone project to local 191 | git clone git@github.com:/stylus-converter.git 192 | 193 | 2. Enter the project directory 194 | cd stylus-converter 195 | 196 | 3. Installation project depends 197 | npm install 198 | 199 | 4. Package compilation source file 200 | npm run build 201 | 202 | 5. Local debugging cli 203 | npm link 204 | ``` 205 | -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const { converter } = require('../lib') 4 | const expect = require('chai').expect 5 | const convertVueFile = require('../bin/convertVueFile') 6 | 7 | function getPath(address) { 8 | return path.resolve(__dirname, address) 9 | } 10 | 11 | describe('测试 CSS Selector', () => { 12 | it('CSS Selector', done => { 13 | fs.readFile(getPath('./stylus/selector.styl'), (err, res) => { 14 | if (err) return 15 | const result = res.toString() 16 | const scss = converter(result) 17 | fs.readFile(getPath('./scss/selector.scss'), (err, sres) => { 18 | if (err) return 19 | const toText = sres.toString() 20 | expect(scss).to.be.equal(toText) 21 | done() 22 | }) 23 | }) 24 | }) 25 | }) 26 | 27 | describe('测试 @Keyframes', () => { 28 | it('Autoprefixer Keyframes', done => { 29 | fs.readFile(getPath('./stylus/keyframes.styl'), (err, res) => { 30 | if (err) return 31 | const result = res.toString() 32 | const scss = converter(result) 33 | fs.readFile(getPath('./scss/autoprefixer-keyframes.scss'), (err, sres) => { 34 | if (err) return 35 | const toText = sres.toString() 36 | expect(scss).to.be.equal(toText) 37 | done() 38 | }) 39 | }) 40 | }) 41 | 42 | it('Not Autoprefixer Keyframes', done => { 43 | fs.readFile(getPath('./stylus/keyframes.styl'), (err, res) => { 44 | if (err) return 45 | const result = res.toString() 46 | const scss = converter(result, { autoprefixer: false }) 47 | fs.readFile(getPath('./scss/keyframes.scss'), (err, sres) => { 48 | if (err) return 49 | const toText = sres.toString() 50 | expect(scss).to.be.equal(toText) 51 | done() 52 | }) 53 | }) 54 | }) 55 | }) 56 | 57 | 58 | describe('测试 @Extend', () => { 59 | it('test @extend', done => { 60 | fs.readFile(getPath('./stylus/extend.styl'), (err, res) => { 61 | if (err) return 62 | const result = res.toString() 63 | const scss = converter(result) 64 | fs.readFile(getPath('./scss/extend.scss'), (err, sres) => { 65 | if (err) return 66 | const toText = sres.toString() 67 | expect(scss).to.be.equal(toText) 68 | done() 69 | }) 70 | }) 71 | }) 72 | }) 73 | 74 | describe('测试 @Media', () => { 75 | it('test @media', done => { 76 | fs.readFile(getPath('./stylus/media.styl'), (err, res) => { 77 | if (err) return 78 | const result = res.toString() 79 | const scss = converter(result) 80 | fs.readFile(getPath('./scss/media.scss'), (err, sres) => { 81 | if (err) return 82 | const toText = sres.toString() 83 | expect(scss).to.be.equal(toText) 84 | done() 85 | }) 86 | }) 87 | }) 88 | }) 89 | 90 | describe('测试 Funciton 与 Params', () => { 91 | it('test params', done => { 92 | fs.readFile(getPath('./stylus/params.styl'), (err, res) => { 93 | if (err) return 94 | const result = res.toString() 95 | const scss = converter(result) 96 | fs.readFile(getPath('./scss/params.scss'), (err, sres) => { 97 | if (err) return 98 | const toText = sres.toString() 99 | expect(scss).to.be.equal(toText) 100 | done() 101 | }) 102 | }) 103 | }) 104 | 105 | it('test variable parameter', done => { 106 | fs.readFile(getPath('./stylus/variable-parameter.styl'), (err, res) => { 107 | if (err) return 108 | const result = res.toString() 109 | const scss = converter(result) 110 | fs.readFile(getPath('./scss/variable-parameter.scss'), (err, sres) => { 111 | if (err) return 112 | const toText = sres.toString() 113 | expect(scss).to.be.equal(toText) 114 | done() 115 | }) 116 | }) 117 | }) 118 | }) 119 | 120 | describe('测试 Comments', () => { 121 | it('test comment', done => { 122 | fs.readFile(getPath('./stylus/comment.styl'), (err, res) => { 123 | if (err) return 124 | const result = res.toString() 125 | const scss = converter(result, { isSignComment: true }) 126 | fs.readFile(getPath('./scss/comment.scss'), (err, sres) => { 127 | if (err) return 128 | const toText = sres.toString() 129 | expect(scss).to.be.equal(toText) 130 | done() 131 | }) 132 | }) 133 | }) 134 | }) 135 | 136 | describe('测试 Mixins', () => { 137 | it('test mixin', done => { 138 | fs.readFile(getPath('./stylus/mixins.styl'), (err, res) => { 139 | if (err) return 140 | const result = res.toString() 141 | const scss = converter(result) 142 | fs.readFile(getPath('./scss/mixins.scss'), (err, sres) => { 143 | if (err) return 144 | const toText = sres.toString() 145 | expect(scss).to.be.equal(toText) 146 | done() 147 | }) 148 | }) 149 | }) 150 | }) 151 | 152 | describe('测试 Variables', () => { 153 | it('test variable assignment', done => { 154 | fs.readFile(getPath('./stylus/variables.styl'), (err, res) => { 155 | if (err) return 156 | const result = res.toString() 157 | const scss = converter(result) 158 | fs.readFile(getPath('./scss/variables.scss'), (err, sres) => { 159 | if (err) return 160 | const toText = sres.toString() 161 | expect(scss).to.be.equal(toText) 162 | done() 163 | }) 164 | }) 165 | }) 166 | 167 | it('test variable search', done => { 168 | fs.readFile(getPath('./stylus/variable-search.styl'), (err, res) => { 169 | if (err) return 170 | const result = res.toString() 171 | const scss = converter(result) 172 | fs.readFile(getPath('./scss/variable-search.scss'), (err, sres) => { 173 | if (err) return 174 | const toText = sres.toString() 175 | expect(scss).to.be.equal(toText) 176 | done() 177 | }) 178 | }) 179 | }) 180 | }) 181 | 182 | describe('测试 Each', () => { 183 | it('test each', done => { 184 | fs.readFile(getPath('./stylus/each.styl'), (err, res) => { 185 | if (err) return 186 | const result = res.toString() 187 | const scss = converter(result) 188 | fs.readFile(getPath('./scss/each.scss'), (err, sres) => { 189 | if (err) return 190 | const toText = sres.toString() 191 | expect(scss).to.be.equal(toText) 192 | done() 193 | }) 194 | }) 195 | }) 196 | }) 197 | 198 | describe('测试 @Font-face', () => { 199 | it('test @font-face', done => { 200 | fs.readFile(getPath('./stylus/font-face.styl'), (err, res) => { 201 | if (err) return 202 | const result = res.toString() 203 | const scss = converter(result) 204 | fs.readFile(getPath('./scss/font-face.scss'), (err, sres) => { 205 | if (err) return 206 | const toText = sres.toString() 207 | expect(scss).to.be.equal(toText) 208 | done() 209 | }) 210 | }) 211 | }) 212 | }) 213 | 214 | describe('测试 Object', () => { 215 | it('test object', done => { 216 | fs.readFile(getPath('./stylus/object.styl'), (err, res) => { 217 | if (err) return 218 | const result = res.toString() 219 | const scss = converter(result) 220 | fs.readFile(getPath('./scss/object.scss'), (err, sres) => { 221 | if (err) return 222 | const toText = sres.toString() 223 | expect(scss).to.be.equal(toText) 224 | done() 225 | }) 226 | }) 227 | }) 228 | }) 229 | 230 | describe('测试 @IF and @else', () => { 231 | it('test if/else', done => { 232 | fs.readFile(getPath('./stylus/if.styl'), (err, res) => { 233 | if (err) return 234 | const result = res.toString() 235 | const scss = converter(result) 236 | fs.readFile(getPath('./scss/if.scss'), (err, sres) => { 237 | if (err) return 238 | const toText = sres.toString() 239 | expect(scss).to.be.equal(toText) 240 | done() 241 | }) 242 | }) 243 | }) 244 | }) 245 | 246 | describe('测试 Boolean', () => { 247 | it('test boolean opeartion', done => { 248 | fs.readFile(getPath('./stylus/boolean.styl'), (err, res) => { 249 | if (err) return 250 | const result = res.toString() 251 | const scss = converter(result) 252 | fs.readFile(getPath('./scss/boolean.scss'), (err, sres) => { 253 | if (err) return 254 | const toText = sres.toString() 255 | expect(scss).to.be.equal(toText) 256 | done() 257 | }) 258 | }) 259 | }) 260 | }) 261 | 262 | describe('测试 @Charset', () => { 263 | it('test charset', done => { 264 | fs.readFile(getPath('./stylus/charset.styl'), (err, res) => { 265 | if (err) return 266 | const result = res.toString() 267 | const scss = converter(result) 268 | fs.readFile(getPath('./scss/charset.scss'), (err, sres) => { 269 | if (err) return 270 | const toText = sres.toString() 271 | expect(scss).to.be.equal(toText) 272 | done() 273 | }) 274 | }) 275 | }) 276 | }) 277 | 278 | describe('测试 @Namescope', () => { 279 | it('test namescope', done => { 280 | fs.readFile(getPath('./stylus/namescope.styl'), (err, res) => { 281 | if (err) return 282 | const result = res.toString() 283 | const scss = converter(result) 284 | fs.readFile(getPath('./scss/namescope.scss'), (err, sres) => { 285 | if (err) return 286 | const toText = sres.toString() 287 | expect(scss).to.be.equal(toText) 288 | done() 289 | }) 290 | }) 291 | }) 292 | }) 293 | 294 | describe('测试 @Page', () => { 295 | it('test page', done => { 296 | fs.readFile(getPath('./stylus/page.styl'), (err, res) => { 297 | if (err) return 298 | const result = res.toString() 299 | const scss = converter(result) 300 | fs.readFile(getPath('./scss/page.scss'), (err, sres) => { 301 | if (err) return 302 | const toText = sres.toString() 303 | expect(scss).to.be.equal(toText) 304 | done() 305 | }) 306 | }) 307 | }) 308 | }) 309 | 310 | describe('测试 @Supports', () => { 311 | it('test supports', done => { 312 | fs.readFile(getPath('./stylus/supports.styl'), (err, res) => { 313 | if (err) return 314 | const result = res.toString() 315 | const scss = converter(result) 316 | fs.readFile(getPath('./scss/supports.scss'), (err, sres) => { 317 | if (err) return 318 | const toText = sres.toString() 319 | expect(scss).to.be.equal(toText) 320 | done() 321 | }) 322 | }) 323 | }) 324 | }) 325 | 326 | describe('测试 @return', () => { 327 | it('test return', done => { 328 | fs.readFile(getPath('./stylus/return.styl'), (err, res) => { 329 | if (err) return 330 | const result = res.toString() 331 | const scss = converter(result) 332 | fs.readFile(getPath('./scss/return.scss'), (err, sres) => { 333 | if (err) return 334 | const toText = sres.toString() 335 | expect(scss).to.be.equal(toText) 336 | done() 337 | }) 338 | }) 339 | }) 340 | }) 341 | 342 | describe('测试 @Import', () => { 343 | it('test @import', done => { 344 | fs.readFile(getPath('./stylus/import.styl'), (err, res) => { 345 | if (err) return 346 | const result = res.toString() 347 | const scss = converter(result) 348 | fs.readFile(getPath('./scss/import.scss'), (err, sres) => { 349 | if (err) return 350 | const toText = sres.toString() 351 | expect(scss).to.be.equal(toText) 352 | done() 353 | }) 354 | }) 355 | }) 356 | }) 357 | 358 | describe('A vue file', () => { 359 | it('should be converted correctly', done => { 360 | fs.readFile(getPath('./vue/stylus/basic.vue'), (err, res) => { 361 | if (err) return 362 | const result = res.toString() 363 | const scss = convertVueFile(result) 364 | fs.readFile(getPath('./vue/scss/basic.vue'), (err, sres) => { 365 | if (err) return 366 | const toText = sres.toString() 367 | expect(scss).to.be.equal(toText) 368 | done() 369 | }) 370 | }) 371 | }) 372 | 373 | it('should be converted deep selectors', done => { 374 | fs.readFile(getPath('./vue/stylus/deep.vue'), (err, res) => { 375 | if (err) return 376 | const result = res.toString() 377 | const scss = convertVueFile(result) 378 | fs.readFile(getPath('./vue/scss/deep.vue'), (err, sres) => { 379 | if (err) return 380 | const toText = sres.toString() 381 | expect(scss).to.be.equal(toText) 382 | done() 383 | }) 384 | }) 385 | }) 386 | 387 | it('should retain it\'s style scoped attribute', done => { 388 | fs.readFile(getPath('./vue/stylus/scoped.vue'), (err, res) => { 389 | if (err) return 390 | const result = res.toString() 391 | const scss = convertVueFile(result) 392 | fs.readFile(getPath('./vue/scss/scoped.vue'), (err, sres) => { 393 | if (err) return 394 | const toText = sres.toString() 395 | expect(scss).to.be.equal(toText) 396 | done() 397 | }) 398 | }) 399 | }) 400 | 401 | it('should handle indentation of the style block ', done => { 402 | fs.readFile(getPath('./vue/stylus/indented.vue'), (err, res) => { 403 | if (err) return 404 | const result = res.toString() 405 | const scss = convertVueFile(result, { indentVueStyleBlock: 2 }); 406 | fs.readFile(getPath('./vue/scss/indented.vue'), (err, sres) => { 407 | if (err) return 408 | const toText = sres.toString() 409 | expect(scss).to.be.equal(toText) 410 | done() 411 | }) 412 | }) 413 | }) 414 | 415 | it('should handle multiple style blocks', done => { 416 | fs.readFile(getPath('./vue/stylus/multiple-style-blocks.vue'), (err, res) => { 417 | if (err) return 418 | const result = res.toString() 419 | const scss = convertVueFile(result); 420 | fs.readFile(getPath('./vue/scss/multiple-style-blocks.vue'), (err, sres) => { 421 | if (err) return 422 | const toText = sres.toString() 423 | expect(scss).to.be.equal(toText) 424 | done() 425 | }) 426 | }) 427 | }) 428 | 429 | it('should handle handle empty style blocks', done => { 430 | fs.readFile(getPath('./vue/stylus/empty.vue'), (err, res) => { 431 | if (err) return 432 | const result = res.toString() 433 | const scss = convertVueFile(result, { indentVueStyleBlock: 2 }); 434 | fs.readFile(getPath('./vue/scss/empty.vue'), (err, sres) => { 435 | if (err) return 436 | const toText = sres.toString() 437 | expect(scss).to.be.equal(toText) 438 | done() 439 | }) 440 | }) 441 | }) 442 | }) 443 | 444 | describe('测试 @Functions', () => { 445 | it('test @functions', done => { 446 | fs.readFile(getPath('./stylus/functions.styl'), (err, res) => { 447 | if (err) return 448 | const result = res.toString() 449 | const scss = converter(result) 450 | fs.readFile(getPath('./scss/functions.scss'), (err, sres) => { 451 | if (err) return 452 | const toText = sres.toString() 453 | expect(scss).to.be.equal(toText) 454 | done() 455 | }) 456 | }) 457 | }) 458 | }) 459 | -------------------------------------------------------------------------------- /src/visitor/index.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant' 2 | import { 3 | _get, 4 | trimFirst, 5 | nodesToJSON, 6 | repeatString, 7 | getCharLength, 8 | replaceFirstATSymbol 9 | } from '../util.js' 10 | 11 | let quote = `'` 12 | let callName = '' 13 | let oldLineno = 1 14 | let paramsLength = 0 15 | let returnSymbol = '' 16 | let indentationLevel = 0 17 | let OBJECT_KEY_LIST = [] 18 | let FUNCTION_PARAMS = [] 19 | let PROPERTY_LIST = [] 20 | let VARIABLE_NAME_LIST = [] 21 | let GLOBAL_MIXIN_NAME_LIST = [] 22 | let GLOBAL_VARIABLE_NAME_LIST = [] 23 | let lastPropertyLineno = 0 24 | let lastPropertyLength = 0 25 | 26 | let isCall = false 27 | let isCond = false 28 | let isNegate = false 29 | let isObject = false 30 | let isFunction = false 31 | let isProperty = false 32 | let isNamespace = false 33 | let isKeyframes = false 34 | let isArguments = false 35 | let isExpression = false 36 | let isCallParams = false 37 | let isIfExpression = false 38 | 39 | let isBlock = false 40 | let ifLength = 0 41 | let binOpLength = 0 42 | let identLength = 0 43 | let selectorLength = 0 44 | let nodesIndex = 0 45 | let nodesLength = 0 46 | 47 | let autoprefixer = true 48 | 49 | const COMPIL_CONFIT = { 50 | scss: { 51 | variable: '$' 52 | }, 53 | less: { 54 | variable: '@' 55 | } 56 | } 57 | 58 | const OPEARTION_MAP = { 59 | '&&': 'and', 60 | '!': 'not', 61 | '||': 'or' 62 | } 63 | 64 | const KEYFRAMES_LIST = [ 65 | '@-webkit-keyframes ', 66 | '@-moz-keyframes ', 67 | '@-ms-keyframes ', 68 | '@-o-keyframes ', 69 | '@keyframes ' 70 | ] 71 | 72 | const TYPE_VISITOR_MAP = { 73 | If: visitIf, 74 | Null: visitNull, 75 | Each: visitEach, 76 | RGBA: visitRGBA, 77 | Unit: visitUnit, 78 | Call: visitCall, 79 | Block: visitBlock, 80 | BinOp: visitBinOp, 81 | Ident: visitIdent, 82 | Group: visitGroup, 83 | Query: visitQuery, 84 | Media: visitMedia, 85 | Import: visitImport, 86 | Atrule: visitAtrule, 87 | Extend: visitExtend, 88 | Member: visitMember, 89 | Return: visitReturn, 90 | 'Object': visitObject, 91 | 'String': visitString, 92 | Feature: visitFeature, 93 | Ternary: visitTernary, 94 | UnaryOp: visitUnaryOp, 95 | Literal: visitLiteral, 96 | Charset: visitCharset, 97 | Params: visitArguments, 98 | 'Comment': visitComment, 99 | Property: visitProperty, 100 | 'Boolean': visitBoolean, 101 | Selector: visitSelector, 102 | Supports: visitSupports, 103 | 'Function': visitFunction, 104 | Arguments: visitArguments, 105 | Keyframes: visitKeyframes, 106 | QueryList: visitQueryList, 107 | Namespace: visitNamespace, 108 | Expression: visitExpression 109 | } 110 | 111 | function handleLineno(lineno) { 112 | return repeatString('\n', lineno - oldLineno) 113 | } 114 | 115 | function trimFnSemicolon(res) { 116 | return res.replace(/\);/g, ')') 117 | } 118 | 119 | function trimSemicolon(res, symbol = '') { 120 | return res.replace(/;/g, '') + symbol 121 | } 122 | 123 | function isCallMixin() { 124 | return !ifLength && !isProperty && !isObject && !isNamespace && !isKeyframes && !isArguments && !identLength && !isCond && !isCallParams && !returnSymbol 125 | } 126 | 127 | function isFunctinCallMixin(node) { 128 | if (node.__type === 'Call') { 129 | return node.block.scope || GLOBAL_MIXIN_NAME_LIST.indexOf(node.name) > -1 130 | } else { 131 | return node.__type === 'If' && isFunctionMixin(node.block.nodes) 132 | } 133 | } 134 | 135 | function hasPropertyOrGroup(node) { 136 | return node.__type === 'Property' || node.__type === 'Group' || node.__type === 'Atrule' || node.__type === 'Media' 137 | } 138 | 139 | function isFunctionMixin(nodes) { 140 | invariant(nodes, 'Missing nodes param'); 141 | const jsonNodes = nodesToJSON(nodes) 142 | return jsonNodes.some(node => hasPropertyOrGroup(node) || isFunctinCallMixin(node)) 143 | } 144 | 145 | function getIndentation() { 146 | return repeatString(' ', indentationLevel * 2) 147 | } 148 | 149 | function handleLinenoAndIndentation({ lineno }) { 150 | return handleLineno(lineno) + getIndentation() 151 | } 152 | 153 | function findNodesType(list, type) { 154 | const nodes = nodesToJSON(list) 155 | return nodes.find(node => node.__type === type) 156 | } 157 | 158 | function visitNode(node) { 159 | if (!node) return '' 160 | if (!node.nodes) { 161 | // guarantee to be an array 162 | node.nodes = [] 163 | } 164 | const json = node.__type ? node : node.toJSON && node.toJSON() 165 | const handler = TYPE_VISITOR_MAP[json.__type] 166 | return handler ? handler(node) : '' 167 | } 168 | 169 | function recursiveSearchName(data, property, name) { 170 | return data[property] 171 | ? recursiveSearchName(data[property], property, name) 172 | : data[name] 173 | } 174 | 175 | // 处理 nodes 176 | function visitNodes(list = []) { 177 | let text = '' 178 | const nodes = nodesToJSON(list) 179 | nodesLength = nodes.length 180 | nodes.forEach((node, i) => { 181 | nodesIndex = i 182 | if (node.__type === 'Comment') { 183 | const isInlineComment = nodes[i - 1] && (nodes[i - 1].lineno === node.lineno); 184 | text += visitComment(node, isInlineComment); 185 | } else { 186 | text += visitNode(node); 187 | } 188 | }); 189 | nodesIndex = 0 190 | nodesLength = 0 191 | return text; 192 | } 193 | 194 | function visitNull() { 195 | return null 196 | } 197 | 198 | // 处理 import;handler import 199 | function visitImport(node) { 200 | invariant(node, 'Missing node param'); 201 | const before = handleLineno(node.lineno) + '@import ' 202 | oldLineno = node.lineno 203 | let quote = '' 204 | let text = '' 205 | const nodes = nodesToJSON(node.path.nodes || []) 206 | nodes.forEach(node => { 207 | text += node.val 208 | if (!quote && node.quote) quote = node.quote 209 | }) 210 | const result = text.replace(/\.styl$/g, '.scss') 211 | return `${before}${quote}${result}${quote};` 212 | } 213 | 214 | function visitSelector(node) { 215 | selectorLength++ 216 | invariant(node, 'Missing node param'); 217 | const nodes = nodesToJSON(node.segments) 218 | const endNode = nodes[nodes.length - 1] 219 | let before = '' 220 | if (endNode.lineno) { 221 | before = handleLineno(endNode.lineno) 222 | oldLineno = endNode.lineno 223 | } 224 | before += getIndentation() 225 | const segmentText = visitNodes(node.segments) 226 | selectorLength-- 227 | return before + segmentText 228 | } 229 | 230 | function visitGroup(node) { 231 | invariant(node, 'Missing node param'); 232 | const before = handleLinenoAndIndentation(node) 233 | oldLineno = node.lineno 234 | const nodes = nodesToJSON(node.nodes) 235 | let selector = '' 236 | nodes.forEach((node, idx) => { 237 | const temp = visitNode(node) 238 | const result = /^\n/.test(temp) ? temp : temp.replace(/^\s*/, '') 239 | selector += idx ? ', ' + result : result 240 | }) 241 | const block = visitBlock(node.block) 242 | if (isKeyframes && /-|\*|\+|\/|\$/.test(selector)) { 243 | const len = getCharLength(selector, ' ') - 2 244 | return `\n${repeatString(' ', len)}#{${trimFirst(selector)}}${block}` 245 | } 246 | return selector + block 247 | } 248 | 249 | function visitBlock(node) { 250 | isBlock = true 251 | invariant(node, 'Missing node param'); 252 | indentationLevel++ 253 | const before = ' {' 254 | const after = `\n${repeatString(' ', (indentationLevel - 1) * 2)}}` 255 | const text = visitNodes(node.nodes) 256 | let result = text 257 | if (isFunction && !/@return/.test(text)) { 258 | result = '' 259 | const symbol = repeatString(' ', indentationLevel * 2) 260 | if (!/\n/.test(text)) { 261 | result += '\n' 262 | oldLineno++ 263 | } 264 | if (!/\s/.test(text)) result += symbol 265 | result += returnSymbol + text 266 | } 267 | if (!/^\n\s*/.test(result)) result = '\n' + repeatString(' ', indentationLevel * 2) + result 268 | indentationLevel-- 269 | isBlock = false 270 | return `${before}${result}${after}` 271 | } 272 | 273 | function visitLiteral(node) { 274 | invariant(node, 'Missing node param'); 275 | return node.val || '' 276 | } 277 | 278 | function visitProperty({ expr, lineno, segments }) { 279 | const suffix = ';' 280 | const before = handleLinenoAndIndentation({ lineno }) 281 | oldLineno = lineno 282 | isProperty = true 283 | const segmentsText = visitNodes(segments) 284 | 285 | lastPropertyLineno = lineno 286 | // segmentsText length plus semicolon and space 287 | lastPropertyLength = segmentsText.length + 2 288 | if (_get(expr, ['nodes', 'length']) === 1) { 289 | const expNode = expr.nodes[0] 290 | const ident = expNode.toJSON && expNode.toJSON() || {} 291 | if (ident.__type === 'Ident') { 292 | const identVal = _get(ident, ['val', 'toJSON']) && ident.val.toJSON() || {} 293 | if (identVal.__type === 'Expression') { 294 | const beforeExpText = before + trimFirst(visitExpression(expr)) 295 | const expText = `${before}${segmentsText}: $${ident.name};` 296 | isProperty = false 297 | PROPERTY_LIST.unshift({ prop: segmentsText, value: '$' + ident.name }) 298 | return beforeExpText + expText 299 | } 300 | } 301 | } 302 | const expText = visitExpression(expr) 303 | PROPERTY_LIST.unshift({ prop: segmentsText, value: expText }) 304 | isProperty = false 305 | return /\/\//.test(expText) 306 | ? `${before + segmentsText.replace(/^$/, '')}: ${expText}` 307 | : trimSemicolon(`${before + segmentsText.replace(/^$/, '')}: ${expText + suffix}`, ';') 308 | } 309 | 310 | function visitIdent({ val, name, rest, mixin, property }) { 311 | identLength++ 312 | const identVal = val && val.toJSON() || '' 313 | if (identVal.__type === 'Null' || !val) { 314 | if (isExpression) { 315 | if (property || isCall) { 316 | const propertyVal = PROPERTY_LIST.find(item => item.prop === name) 317 | if (propertyVal) { 318 | identLength-- 319 | return propertyVal.value 320 | } 321 | } 322 | } 323 | if (selectorLength && isExpression && !binOpLength) { 324 | identLength-- 325 | return `#{${name}}` 326 | } 327 | if (mixin) { 328 | identLength-- 329 | return name === 'block' ? '@content;' : `#{$${name}}` 330 | } 331 | let nameText = (VARIABLE_NAME_LIST.indexOf(name) > -1 || GLOBAL_VARIABLE_NAME_LIST.indexOf(name) > -1) 332 | ? replaceFirstATSymbol(name) 333 | : name 334 | if (FUNCTION_PARAMS.indexOf(name) > -1) nameText = replaceFirstATSymbol(nameText) 335 | identLength-- 336 | return rest ? `${nameText}...` : nameText 337 | } 338 | if (identVal.__type === 'Expression') { 339 | if (findNodesType(identVal.nodes, 'Object')) OBJECT_KEY_LIST.push(name) 340 | const before = handleLinenoAndIndentation(identVal) 341 | oldLineno = identVal.lineno 342 | const nodes = nodesToJSON(identVal.nodes || []) 343 | let expText = '' 344 | nodes.forEach((node, idx) => { 345 | expText += idx ? ` ${visitNode(node)}` : visitNode(node) 346 | }) 347 | VARIABLE_NAME_LIST.push(name) 348 | identLength-- 349 | return `${before}${replaceFirstATSymbol(name)}: ${trimFnSemicolon(expText)};` 350 | } 351 | if (identVal.__type === 'Function') { 352 | identLength-- 353 | return visitFunction(identVal) 354 | } 355 | let identText = visitNode(identVal) 356 | identLength-- 357 | return `${replaceFirstATSymbol(name)}: ${identText};` 358 | } 359 | 360 | function visitExpression(node) { 361 | invariant(node, 'Missing node param'); 362 | isExpression = true 363 | const nodes = nodesToJSON(node.nodes) 364 | const comments = [] 365 | let subLineno = 0 366 | let result = '' 367 | let before = '' 368 | 369 | if (nodes.every(node => node.__type !== 'Expression')) { 370 | subLineno = nodes.map(node => node.lineno).sort((curr, next) => next - curr)[0] 371 | } 372 | 373 | let space = '' 374 | if (subLineno > node.lineno) { 375 | before = handleLineno(subLineno) 376 | oldLineno = subLineno 377 | if (subLineno > lastPropertyLineno) space = repeatString(' ', lastPropertyLength) 378 | } else { 379 | before = handleLineno(node.lineno) 380 | const callNode = nodes.find(node => node.__type === 'Call') 381 | if (callNode && !isObject && !isCallMixin()) space = repeatString(' ', lastPropertyLength) 382 | oldLineno = node.lineno 383 | } 384 | 385 | nodes.forEach((node, idx) => { 386 | // handle inline comment 387 | if (node.__type === 'Comment') { 388 | comments.push(node) 389 | } else { 390 | const nodeText = visitNode(node) 391 | const symbol = isProperty && node.nodes.length ? ',' : '' 392 | result += idx ? symbol + ' ' + nodeText : nodeText 393 | } 394 | }) 395 | 396 | let commentText = comments.map(node => visitNode(node)).join(' ') 397 | commentText = commentText.replace(/^ +/, ' ') 398 | 399 | isExpression = false 400 | 401 | if (isProperty && /\);/g.test(result)) result = trimFnSemicolon(result) + ';' 402 | if (commentText) result = result + ';' + commentText 403 | if (isCall || binOpLength) { 404 | if (callName === 'url') return result.replace(/\s/g, '') 405 | return result 406 | } 407 | 408 | if (!returnSymbol || isIfExpression) { 409 | return (before && space) ? trimSemicolon(before + getIndentation() + space + result, ';') : result 410 | } 411 | let symbol = '' 412 | if (nodesIndex + 1 === nodesLength) symbol = returnSymbol 413 | return before + getIndentation() + symbol + result 414 | } 415 | 416 | function visitCall({ name, args, lineno, block }) { 417 | isCall = true 418 | callName = name 419 | let blockText = '' 420 | let before = handleLineno(lineno) 421 | oldLineno = lineno 422 | if (isCallMixin() || block || selectorLength || GLOBAL_MIXIN_NAME_LIST.indexOf(callName) > -1) { 423 | before = before || '\n' 424 | before += getIndentation() 425 | before += '@include ' 426 | } 427 | const argsText = visitArguments(args).replace(/;/g, '') 428 | isCallParams = false 429 | if (block) blockText = visitBlock(block) 430 | callName = '' 431 | isCall = false 432 | return `${before + name}(${argsText})${blockText};` 433 | } 434 | 435 | function visitArguments(node) { 436 | invariant(node, 'Missing node param'); 437 | isArguments = true 438 | const nodes = nodesToJSON(node.nodes) 439 | paramsLength += nodes.length 440 | let text = '' 441 | nodes.forEach((node, idx) => { 442 | const prefix = idx ? ', ' : '' 443 | let nodeText = visitNode(node) 444 | if (node.__type === 'Call') isCallParams = true 445 | if (GLOBAL_VARIABLE_NAME_LIST.indexOf(nodeText) > -1) nodeText = replaceFirstATSymbol(nodeText) 446 | if (isFunction && !/(^'|")|\d/.test(nodeText) && nodeText) nodeText = replaceFirstATSymbol(nodeText) 447 | text += prefix + nodeText 448 | paramsLength-- 449 | }) 450 | if (paramsLength === 0) isArguments = false 451 | return text || '' 452 | } 453 | 454 | function visitRGBA(node) { 455 | return node.raw.replace(/ /g, '') 456 | } 457 | 458 | function visitUnit({ val, type }) { 459 | return type ? val + type : val 460 | } 461 | 462 | function visitBoolean(node) { 463 | return node.val 464 | } 465 | 466 | function visitIf(node, symbol = '@if ') { 467 | ifLength++ 468 | invariant(node, 'Missing node param'); 469 | let before = '' 470 | isIfExpression = true 471 | if (symbol === '@if ') { 472 | before += handleLinenoAndIndentation(node) 473 | oldLineno = node.lineno 474 | } 475 | 476 | const condNode = node.cond && node.cond.toJSON() || {} 477 | isCond = true 478 | isNegate = node.negate 479 | const condText = trimSemicolon(visitNode(condNode)) 480 | isCond = false 481 | isNegate = false 482 | isIfExpression = false 483 | const block = visitBlock(node.block) 484 | let elseText = '' 485 | if (node.elses && node.elses.length) { 486 | const elses = nodesToJSON(node.elses) 487 | elses.forEach(node => { 488 | oldLineno++ 489 | if (node.__type === 'If') { 490 | elseText += visitIf(node, ' @else if ') 491 | } else { 492 | elseText += ' @else' + visitBlock(node) 493 | } 494 | }) 495 | } 496 | ifLength-- 497 | return before + symbol + condText + block + elseText 498 | } 499 | 500 | function visitFunction(node) { 501 | invariant(node, 'Missing node param'); 502 | isFunction = true 503 | const notMixin = !isFunctionMixin(node.block.nodes) 504 | let before = handleLineno(node.lineno) 505 | oldLineno = node.lineno 506 | let symbol = '' 507 | if (notMixin) { 508 | returnSymbol = '@return ' 509 | symbol = '@function' 510 | } else { 511 | returnSymbol = '' 512 | symbol = '@mixin' 513 | } 514 | const params = nodesToJSON(node.params.nodes || []) 515 | FUNCTION_PARAMS = params.map(par => par.name) 516 | let paramsText = '' 517 | params.forEach((node, idx) => { 518 | const prefix = idx ? ', ' : '' 519 | const nodeText = visitNode(node) 520 | VARIABLE_NAME_LIST.push(nodeText) 521 | paramsText += prefix + replaceFirstATSymbol(nodeText) 522 | }) 523 | paramsText = paramsText.replace(/\$ +\$/g, '$') 524 | const fnName = `${symbol} ${node.name}(${trimSemicolon(paramsText)})` 525 | const block = visitBlock(node.block) 526 | returnSymbol = '' 527 | isFunction = false 528 | FUNCTION_PARAMS = [] 529 | return before + fnName + block 530 | } 531 | 532 | function visitTernary({ cond, lineno }) { 533 | let before = handleLineno(lineno) 534 | oldLineno = lineno 535 | return before + visitBinOp(cond) 536 | } 537 | 538 | function visitBinOp({ op, left, right }) { 539 | binOpLength++ 540 | function visitNegate(op) { 541 | if (!isNegate || (op !== '==' && op !== '!=')) { 542 | return op !== 'is defined' ? op : '' 543 | } 544 | return op === '==' ? '!=' : '==' 545 | } 546 | 547 | if (op === '[]') { 548 | const leftText = visitNode(left) 549 | const rightText = visitNode(right) 550 | binOpLength-- 551 | if (isBlock) 552 | return `map-get(${leftText}, ${rightText});` 553 | } 554 | 555 | const leftExp = left ? left.toJSON() : '' 556 | const rightExp = right ? right.toJSON() : '' 557 | const isExp = rightExp.__type === 'Expression' 558 | const expText = isExp ? `(${visitNode(rightExp)})` : visitNode(rightExp) 559 | const symbol = OPEARTION_MAP[op] || visitNegate(op) 560 | const endSymbol = op === 'is defined' ? '!default;' : '' 561 | 562 | binOpLength-- 563 | return endSymbol 564 | ? `${trimSemicolon(visitNode(leftExp)).trim()} ${endSymbol}` 565 | : `${visitNode(leftExp)} ${symbol} ${expText}` 566 | } 567 | 568 | function visitUnaryOp({ op, expr }) { 569 | return `${OPEARTION_MAP[op] || op}(${visitExpression(expr)})` 570 | } 571 | 572 | function visitEach(node) { 573 | invariant(node, 'Missing node param'); 574 | let before = handleLineno(node.lineno) 575 | oldLineno = node.lineno 576 | const expr = node.expr && node.expr.toJSON() 577 | const exprNodes = nodesToJSON(expr.nodes) 578 | let exprText = `@each $${node.val} in ` 579 | VARIABLE_NAME_LIST.push(node.val) 580 | exprNodes.forEach((node, idx) => { 581 | const prefix = node.__type === 'Ident' ? '$' : '' 582 | const exp = prefix + visitNode(node) 583 | exprText += idx ? `, ${exp}` : exp 584 | }) 585 | if (/\.\./.test(exprText)) { 586 | exprText = exprText.replace('@each', '@for').replace('..', 'through').replace('in', 'from') 587 | } 588 | const blank = getIndentation() 589 | before += blank 590 | const block = visitBlock(node.block, blank).replace(`$${node.key}`, '') 591 | return before + exprText + block 592 | } 593 | 594 | function visitKeyframes(node) { 595 | isKeyframes = true 596 | let before = handleLinenoAndIndentation(node) 597 | oldLineno = node.lineno 598 | let resultText = '' 599 | const name = visitNodes(node.segments) 600 | const isMixin = !!findNodesType(node.segments, 'Expression') 601 | const blockJson = node.block.toJSON() 602 | if (blockJson.nodes.length && blockJson.nodes[0].toJSON().__type === 'Expression') { 603 | throw new Error(`Syntax Error Please check if your @keyframes ${name} are correct.`) 604 | } 605 | const block = visitBlock(node.block) 606 | const text = isMixin ? `#{${name}}${block}` : name + block 607 | if (autoprefixer) { 608 | KEYFRAMES_LIST.forEach(name => { 609 | resultText += before + name + text 610 | }) 611 | } else { 612 | resultText += before + '@keyframes ' + text 613 | } 614 | isKeyframes = false 615 | return resultText 616 | } 617 | 618 | function visitExtend(node) { 619 | const before = handleLinenoAndIndentation(node) 620 | oldLineno = node.lineno 621 | const text = visitNodes(node.selectors) 622 | return `${before}@extend ${trimFirst(text)};` 623 | } 624 | 625 | function visitQueryList(node) { 626 | let text = '' 627 | const nodes = nodesToJSON(node.nodes) 628 | nodes.forEach((node, idx) => { 629 | const nodeText = visitNode(node) 630 | text += idx ? `, ${nodeText}` : nodeText 631 | }) 632 | return text 633 | } 634 | 635 | function visitQuery(node) { 636 | const type = visitNode(node.type) || '' 637 | const nodes = nodesToJSON(node.nodes) 638 | let text = '' 639 | nodes.forEach((node, idx) => { 640 | const nodeText = visitNode(node) 641 | text += idx ? ` and ${nodeText}` : nodeText 642 | }) 643 | return type === 'screen' ? `${type} and ${text}` : `${type}${text}` 644 | } 645 | 646 | function visitMedia(node) { 647 | const before = handleLinenoAndIndentation(node) 648 | oldLineno = node.lineno 649 | const val = _get(node, ['val'], {}) 650 | const nodeVal = val.toJSON && val.toJSON() || {} 651 | const valText = visitNode(nodeVal) 652 | const block = visitBlock(node.block) 653 | return `${before}@media ${valText + block}` 654 | } 655 | 656 | function visitFeature(node) { 657 | const segmentsText = visitNodes(node.segments) 658 | const expText = visitExpression(node.expr) 659 | return `(${segmentsText}: ${expText})` 660 | } 661 | 662 | function visitComment(node, isInlineComment) { 663 | const before = isInlineComment ? ' ' : handleLinenoAndIndentation(node); 664 | const matchs = node.str.match(/\n/g) 665 | oldLineno = node.lineno 666 | if (Array.isArray(matchs)) oldLineno += matchs.length 667 | const text = node.suppress ? node.str : node.str.replace(/^\/\*/, '/*!') 668 | return before + text 669 | } 670 | 671 | function visitMember({ left, right }) { 672 | const searchName = recursiveSearchName(left, 'left', 'name') 673 | if (searchName && OBJECT_KEY_LIST.indexOf(searchName) > -1) { 674 | return `map-get(${visitNode(left)}, ${quote + visitNode(right) + quote})` 675 | } 676 | return `${visitNode(left)}.${visitNode(right)}` 677 | } 678 | 679 | function visitAtrule(node) { 680 | let before = handleLinenoAndIndentation(node) 681 | oldLineno = node.lineno 682 | before += '@' + node.type 683 | return before + visitBlock(node.block) 684 | } 685 | 686 | function visitObject({ vals, lineno }) { 687 | isObject = true 688 | indentationLevel++ 689 | const before = repeatString(' ', indentationLevel * 2) 690 | let result = `` 691 | let count = 0 692 | for (let key in vals) { 693 | const resultVal = visitNode(vals[key]).replace(/;/, '') 694 | const symbol = count ? ',' : '' 695 | result += `${symbol}\n${before + quote + key + quote}: ${resultVal}` 696 | count++ 697 | } 698 | const totalLineno = lineno + count + 2 699 | oldLineno = totalLineno > oldLineno ? totalLineno : oldLineno 700 | indentationLevel-- 701 | isObject = false 702 | return `(${result}\n${repeatString(' ', indentationLevel * 2)})` 703 | } 704 | 705 | function visitCharset({ val: { val: value, quote }, lineno }) { 706 | const before = handleLineno(lineno) 707 | oldLineno = lineno 708 | return `${before}@charset ${quote + value + quote};` 709 | } 710 | 711 | function visitNamespace({ val, lineno }) { 712 | isNamespace = true 713 | const name = '@namespace ' 714 | const before = handleLineno(lineno) 715 | oldLineno = lineno 716 | if (val.type === 'string') { 717 | const { val: value, quote: valQuote } = val.val 718 | isNamespace = false 719 | return before + name + valQuote + value + valQuote + ';' 720 | } 721 | return before + name + visitNode(val) 722 | } 723 | 724 | function visitAtrule({ type, block, lineno, segments }) { 725 | const before = handleLineno(lineno) 726 | oldLineno = lineno 727 | const typeText = segments.length ? `@${type} ` : `@${type}` 728 | return `${before + typeText + visitNodes(segments) + visitBlock(block)}` 729 | } 730 | 731 | function visitSupports({ block, lineno, condition }) { 732 | let before = handleLineno(lineno) 733 | oldLineno = lineno 734 | before += getIndentation() 735 | return `${before}@Supports ${visitNode(condition) + visitBlock(block)}` 736 | } 737 | 738 | function visitString({ val, quote }) { 739 | return quote + val + quote 740 | } 741 | 742 | function visitReturn(node) { 743 | if (isFunction) return visitExpression(node.expr).replace(/\n\s*/g, '') 744 | return '@return $' + visitExpression(node.expr).replace(/\$|\n\s*/g, '') 745 | } 746 | 747 | // 处理 stylus 语法树;handle stylus Syntax Tree 748 | export default function visitor(ast, options, globalVariableList, globalMixinList) { 749 | quote = options.quote 750 | autoprefixer = options.autoprefixer 751 | GLOBAL_MIXIN_NAME_LIST = globalMixinList 752 | GLOBAL_VARIABLE_NAME_LIST = globalVariableList 753 | let result = visitNodes(ast.nodes) || '' 754 | const indentation = ' '.repeat(options.indentVueStyleBlock) 755 | result = result.replace(/(.*\S.*)/g, `${indentation}$1`); 756 | result = result.replace(/(.*)>>>(.*)/g, `$1/deep/$2`) 757 | oldLineno = 1 758 | FUNCTION_PARAMS = [] 759 | OBJECT_KEY_LIST = [] 760 | PROPERTY_LIST = [] 761 | VARIABLE_NAME_LIST = [] 762 | GLOBAL_MIXIN_NAME_LIST = [] 763 | GLOBAL_VARIABLE_NAME_LIST = [] 764 | return result + '\n' 765 | } 766 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { value: true }); 4 | 5 | function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } 6 | 7 | var invariant = _interopDefault(require('invariant')); 8 | var Parser = _interopDefault(require('stylus/lib/parser.js')); 9 | 10 | function repeatString(str, num) { 11 | return num > 0 ? str.repeat(num) : ''; 12 | } 13 | 14 | function nodesToJSON(nodes) { 15 | return nodes.map(function (node) { 16 | return Object.assign({ 17 | // default in case not in node 18 | nodes: [] 19 | }, node.toJSON()); 20 | }); 21 | } 22 | 23 | function trimFirst(str) { 24 | return str.replace(/(^\s*)/g, ''); 25 | } 26 | 27 | function replaceFirstATSymbol(str) { 28 | var temp = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '$'; 29 | 30 | return str.replace(/^\$|/, temp); 31 | } 32 | 33 | function getCharLength(str, char) { 34 | return str.split(char).length - 1; 35 | } 36 | 37 | function _get(obj, pathArray, defaultValue) { 38 | if (obj == null) return defaultValue; 39 | 40 | var value = obj; 41 | 42 | pathArray = [].concat(pathArray); 43 | 44 | for (var i = 0; i < pathArray.length; i += 1) { 45 | var key = pathArray[i]; 46 | value = value[key]; 47 | if (value == null) { 48 | return defaultValue; 49 | } 50 | } 51 | 52 | return value; 53 | } 54 | 55 | var quote = '\''; 56 | var callName = ''; 57 | var oldLineno = 1; 58 | var paramsLength = 0; 59 | var returnSymbol = ''; 60 | var indentationLevel = 0; 61 | var OBJECT_KEY_LIST = []; 62 | var FUNCTION_PARAMS = []; 63 | var PROPERTY_LIST = []; 64 | var VARIABLE_NAME_LIST = []; 65 | var GLOBAL_MIXIN_NAME_LIST = []; 66 | var GLOBAL_VARIABLE_NAME_LIST = []; 67 | var lastPropertyLineno = 0; 68 | var lastPropertyLength = 0; 69 | 70 | var isCall = false; 71 | var isCond = false; 72 | var isNegate = false; 73 | var isObject = false; 74 | var isFunction = false; 75 | var isProperty = false; 76 | var isNamespace = false; 77 | var isKeyframes = false; 78 | var isArguments = false; 79 | var isExpression = false; 80 | var isCallParams = false; 81 | var isIfExpression = false; 82 | 83 | var isBlock = false; 84 | var ifLength = 0; 85 | var binOpLength = 0; 86 | var identLength = 0; 87 | var selectorLength = 0; 88 | var nodesIndex = 0; 89 | var nodesLength = 0; 90 | 91 | var autoprefixer = true; 92 | 93 | var OPEARTION_MAP = { 94 | '&&': 'and', 95 | '!': 'not', 96 | '||': 'or' 97 | }; 98 | 99 | var KEYFRAMES_LIST = ['@-webkit-keyframes ', '@-moz-keyframes ', '@-ms-keyframes ', '@-o-keyframes ', '@keyframes ']; 100 | 101 | var TYPE_VISITOR_MAP = { 102 | If: visitIf, 103 | Null: visitNull, 104 | Each: visitEach, 105 | RGBA: visitRGBA, 106 | Unit: visitUnit, 107 | Call: visitCall, 108 | Block: visitBlock, 109 | BinOp: visitBinOp, 110 | Ident: visitIdent, 111 | Group: visitGroup, 112 | Query: visitQuery, 113 | Media: visitMedia, 114 | Import: visitImport, 115 | Atrule: visitAtrule, 116 | Extend: visitExtend, 117 | Member: visitMember, 118 | Return: visitReturn, 119 | 'Object': visitObject, 120 | 'String': visitString, 121 | Feature: visitFeature, 122 | Ternary: visitTernary, 123 | UnaryOp: visitUnaryOp, 124 | Literal: visitLiteral, 125 | Charset: visitCharset, 126 | Params: visitArguments, 127 | 'Comment': visitComment, 128 | Property: visitProperty, 129 | 'Boolean': visitBoolean, 130 | Selector: visitSelector, 131 | Supports: visitSupports, 132 | 'Function': visitFunction, 133 | Arguments: visitArguments, 134 | Keyframes: visitKeyframes, 135 | QueryList: visitQueryList, 136 | Namespace: visitNamespace, 137 | Expression: visitExpression 138 | }; 139 | 140 | function handleLineno(lineno) { 141 | return repeatString('\n', lineno - oldLineno); 142 | } 143 | 144 | function trimFnSemicolon(res) { 145 | return res.replace(/\);/g, ')'); 146 | } 147 | 148 | function trimSemicolon(res) { 149 | var symbol = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; 150 | 151 | return res.replace(/;/g, '') + symbol; 152 | } 153 | 154 | function isCallMixin() { 155 | return !ifLength && !isProperty && !isObject && !isNamespace && !isKeyframes && !isArguments && !identLength && !isCond && !isCallParams && !returnSymbol; 156 | } 157 | 158 | function isFunctinCallMixin(node) { 159 | if (node.__type === 'Call') { 160 | return node.block.scope || GLOBAL_MIXIN_NAME_LIST.indexOf(node.name) > -1; 161 | } else { 162 | return node.__type === 'If' && isFunctionMixin(node.block.nodes); 163 | } 164 | } 165 | 166 | function hasPropertyOrGroup(node) { 167 | return node.__type === 'Property' || node.__type === 'Group' || node.__type === 'Atrule' || node.__type === 'Media'; 168 | } 169 | 170 | function isFunctionMixin(nodes) { 171 | invariant(nodes, 'Missing nodes param'); 172 | var jsonNodes = nodesToJSON(nodes); 173 | return jsonNodes.some(function (node) { 174 | return hasPropertyOrGroup(node) || isFunctinCallMixin(node); 175 | }); 176 | } 177 | 178 | function getIndentation() { 179 | return repeatString(' ', indentationLevel * 2); 180 | } 181 | 182 | function handleLinenoAndIndentation(_ref) { 183 | var lineno = _ref.lineno; 184 | 185 | return handleLineno(lineno) + getIndentation(); 186 | } 187 | 188 | function findNodesType(list, type) { 189 | var nodes = nodesToJSON(list); 190 | return nodes.find(function (node) { 191 | return node.__type === type; 192 | }); 193 | } 194 | 195 | function visitNode(node) { 196 | if (!node) return ''; 197 | if (!node.nodes) { 198 | // guarantee to be an array 199 | node.nodes = []; 200 | } 201 | var json = node.__type ? node : node.toJSON && node.toJSON(); 202 | var handler = TYPE_VISITOR_MAP[json.__type]; 203 | return handler ? handler(node) : ''; 204 | } 205 | 206 | function recursiveSearchName(data, property, name) { 207 | return data[property] ? recursiveSearchName(data[property], property, name) : data[name]; 208 | } 209 | 210 | // 处理 nodes 211 | function visitNodes() { 212 | var list = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 213 | 214 | var text = ''; 215 | var nodes = nodesToJSON(list); 216 | nodesLength = nodes.length; 217 | nodes.forEach(function (node, i) { 218 | nodesIndex = i; 219 | if (node.__type === 'Comment') { 220 | var isInlineComment = nodes[i - 1] && nodes[i - 1].lineno === node.lineno; 221 | text += visitComment(node, isInlineComment); 222 | } else { 223 | text += visitNode(node); 224 | } 225 | }); 226 | nodesIndex = 0; 227 | nodesLength = 0; 228 | return text; 229 | } 230 | 231 | function visitNull() { 232 | return null; 233 | } 234 | 235 | // 处理 import;handler import 236 | function visitImport(node) { 237 | invariant(node, 'Missing node param'); 238 | var before = handleLineno(node.lineno) + '@import '; 239 | oldLineno = node.lineno; 240 | var quote = ''; 241 | var text = ''; 242 | var nodes = nodesToJSON(node.path.nodes || []); 243 | nodes.forEach(function (node) { 244 | text += node.val; 245 | if (!quote && node.quote) quote = node.quote; 246 | }); 247 | var result = text.replace(/\.styl$/g, '.scss'); 248 | return '' + before + quote + result + quote + ';'; 249 | } 250 | 251 | function visitSelector(node) { 252 | selectorLength++; 253 | invariant(node, 'Missing node param'); 254 | var nodes = nodesToJSON(node.segments); 255 | var endNode = nodes[nodes.length - 1]; 256 | var before = ''; 257 | if (endNode.lineno) { 258 | before = handleLineno(endNode.lineno); 259 | oldLineno = endNode.lineno; 260 | } 261 | before += getIndentation(); 262 | var segmentText = visitNodes(node.segments); 263 | selectorLength--; 264 | return before + segmentText; 265 | } 266 | 267 | function visitGroup(node) { 268 | invariant(node, 'Missing node param'); 269 | var before = handleLinenoAndIndentation(node); 270 | oldLineno = node.lineno; 271 | var nodes = nodesToJSON(node.nodes); 272 | var selector = ''; 273 | nodes.forEach(function (node, idx) { 274 | var temp = visitNode(node); 275 | var result = /^\n/.test(temp) ? temp : temp.replace(/^\s*/, ''); 276 | selector += idx ? ', ' + result : result; 277 | }); 278 | var block = visitBlock(node.block); 279 | if (isKeyframes && /-|\*|\+|\/|\$/.test(selector)) { 280 | var len = getCharLength(selector, ' ') - 2; 281 | return '\n' + repeatString(' ', len) + '#{' + trimFirst(selector) + '}' + block; 282 | } 283 | return selector + block; 284 | } 285 | 286 | function visitBlock(node) { 287 | isBlock = true; 288 | invariant(node, 'Missing node param'); 289 | indentationLevel++; 290 | var before = ' {'; 291 | var after = '\n' + repeatString(' ', (indentationLevel - 1) * 2) + '}'; 292 | var text = visitNodes(node.nodes); 293 | var result = text; 294 | if (isFunction && !/@return/.test(text)) { 295 | result = ''; 296 | var symbol = repeatString(' ', indentationLevel * 2); 297 | if (!/\n/.test(text)) { 298 | result += '\n'; 299 | oldLineno++; 300 | } 301 | if (!/\s/.test(text)) result += symbol; 302 | result += returnSymbol + text; 303 | } 304 | if (!/^\n\s*/.test(result)) result = '\n' + repeatString(' ', indentationLevel * 2) + result; 305 | indentationLevel--; 306 | isBlock = false; 307 | return '' + before + result + after; 308 | } 309 | 310 | function visitLiteral(node) { 311 | invariant(node, 'Missing node param'); 312 | return node.val || ''; 313 | } 314 | 315 | function visitProperty(_ref2) { 316 | var expr = _ref2.expr, 317 | lineno = _ref2.lineno, 318 | segments = _ref2.segments; 319 | 320 | var suffix = ';'; 321 | var before = handleLinenoAndIndentation({ lineno: lineno }); 322 | oldLineno = lineno; 323 | isProperty = true; 324 | var segmentsText = visitNodes(segments); 325 | 326 | lastPropertyLineno = lineno; 327 | // segmentsText length plus semicolon and space 328 | lastPropertyLength = segmentsText.length + 2; 329 | if (_get(expr, ['nodes', 'length']) === 1) { 330 | var expNode = expr.nodes[0]; 331 | var ident = expNode.toJSON && expNode.toJSON() || {}; 332 | if (ident.__type === 'Ident') { 333 | var identVal = _get(ident, ['val', 'toJSON']) && ident.val.toJSON() || {}; 334 | if (identVal.__type === 'Expression') { 335 | var beforeExpText = before + trimFirst(visitExpression(expr)); 336 | var _expText = '' + before + segmentsText + ': $' + ident.name + ';'; 337 | isProperty = false; 338 | PROPERTY_LIST.unshift({ prop: segmentsText, value: '$' + ident.name }); 339 | return beforeExpText + _expText; 340 | } 341 | } 342 | } 343 | var expText = visitExpression(expr); 344 | PROPERTY_LIST.unshift({ prop: segmentsText, value: expText }); 345 | isProperty = false; 346 | return (/\/\//.test(expText) ? before + segmentsText.replace(/^$/, '') + ': ' + expText : trimSemicolon(before + segmentsText.replace(/^$/, '') + ': ' + (expText + suffix), ';') 347 | ); 348 | } 349 | 350 | function visitIdent(_ref3) { 351 | var val = _ref3.val, 352 | name = _ref3.name, 353 | rest = _ref3.rest, 354 | mixin = _ref3.mixin, 355 | property = _ref3.property; 356 | 357 | identLength++; 358 | var identVal = val && val.toJSON() || ''; 359 | if (identVal.__type === 'Null' || !val) { 360 | if (isExpression) { 361 | if (property || isCall) { 362 | var propertyVal = PROPERTY_LIST.find(function (item) { 363 | return item.prop === name; 364 | }); 365 | if (propertyVal) { 366 | identLength--; 367 | return propertyVal.value; 368 | } 369 | } 370 | } 371 | if (selectorLength && isExpression && !binOpLength) { 372 | identLength--; 373 | return '#{' + name + '}'; 374 | } 375 | if (mixin) { 376 | identLength--; 377 | return name === 'block' ? '@content;' : '#{$' + name + '}'; 378 | } 379 | var nameText = VARIABLE_NAME_LIST.indexOf(name) > -1 || GLOBAL_VARIABLE_NAME_LIST.indexOf(name) > -1 ? replaceFirstATSymbol(name) : name; 380 | if (FUNCTION_PARAMS.indexOf(name) > -1) nameText = replaceFirstATSymbol(nameText); 381 | identLength--; 382 | return rest ? nameText + '...' : nameText; 383 | } 384 | if (identVal.__type === 'Expression') { 385 | if (findNodesType(identVal.nodes, 'Object')) OBJECT_KEY_LIST.push(name); 386 | var before = handleLinenoAndIndentation(identVal); 387 | oldLineno = identVal.lineno; 388 | var nodes = nodesToJSON(identVal.nodes || []); 389 | var expText = ''; 390 | nodes.forEach(function (node, idx) { 391 | expText += idx ? ' ' + visitNode(node) : visitNode(node); 392 | }); 393 | VARIABLE_NAME_LIST.push(name); 394 | identLength--; 395 | return '' + before + replaceFirstATSymbol(name) + ': ' + trimFnSemicolon(expText) + ';'; 396 | } 397 | if (identVal.__type === 'Function') { 398 | identLength--; 399 | return visitFunction(identVal); 400 | } 401 | var identText = visitNode(identVal); 402 | identLength--; 403 | return replaceFirstATSymbol(name) + ': ' + identText + ';'; 404 | } 405 | 406 | function visitExpression(node) { 407 | invariant(node, 'Missing node param'); 408 | isExpression = true; 409 | var nodes = nodesToJSON(node.nodes); 410 | var comments = []; 411 | var subLineno = 0; 412 | var result = ''; 413 | var before = ''; 414 | 415 | if (nodes.every(function (node) { 416 | return node.__type !== 'Expression'; 417 | })) { 418 | subLineno = nodes.map(function (node) { 419 | return node.lineno; 420 | }).sort(function (curr, next) { 421 | return next - curr; 422 | })[0]; 423 | } 424 | 425 | var space = ''; 426 | if (subLineno > node.lineno) { 427 | before = handleLineno(subLineno); 428 | oldLineno = subLineno; 429 | if (subLineno > lastPropertyLineno) space = repeatString(' ', lastPropertyLength); 430 | } else { 431 | before = handleLineno(node.lineno); 432 | var callNode = nodes.find(function (node) { 433 | return node.__type === 'Call'; 434 | }); 435 | if (callNode && !isObject && !isCallMixin()) space = repeatString(' ', lastPropertyLength); 436 | oldLineno = node.lineno; 437 | } 438 | 439 | nodes.forEach(function (node, idx) { 440 | // handle inline comment 441 | if (node.__type === 'Comment') { 442 | comments.push(node); 443 | } else { 444 | var nodeText = visitNode(node); 445 | var _symbol = isProperty && node.nodes.length ? ',' : ''; 446 | result += idx ? _symbol + ' ' + nodeText : nodeText; 447 | } 448 | }); 449 | 450 | var commentText = comments.map(function (node) { 451 | return visitNode(node); 452 | }).join(' '); 453 | commentText = commentText.replace(/^ +/, ' '); 454 | 455 | isExpression = false; 456 | 457 | if (isProperty && /\);/g.test(result)) result = trimFnSemicolon(result) + ';'; 458 | if (commentText) result = result + ';' + commentText; 459 | if (isCall || binOpLength) { 460 | if (callName === 'url') return result.replace(/\s/g, ''); 461 | return result; 462 | } 463 | 464 | if (!returnSymbol || isIfExpression) { 465 | return before && space ? trimSemicolon(before + getIndentation() + space + result, ';') : result; 466 | } 467 | var symbol = ''; 468 | if (nodesIndex + 1 === nodesLength) symbol = returnSymbol; 469 | return before + getIndentation() + symbol + result; 470 | } 471 | 472 | function visitCall(_ref4) { 473 | var name = _ref4.name, 474 | args = _ref4.args, 475 | lineno = _ref4.lineno, 476 | block = _ref4.block; 477 | 478 | isCall = true; 479 | callName = name; 480 | var blockText = ''; 481 | var before = handleLineno(lineno); 482 | oldLineno = lineno; 483 | if (isCallMixin() || block || selectorLength || GLOBAL_MIXIN_NAME_LIST.indexOf(callName) > -1) { 484 | before = before || '\n'; 485 | before += getIndentation(); 486 | before += '@include '; 487 | } 488 | var argsText = visitArguments(args).replace(/;/g, ''); 489 | isCallParams = false; 490 | if (block) blockText = visitBlock(block); 491 | callName = ''; 492 | isCall = false; 493 | return before + name + '(' + argsText + ')' + blockText + ';'; 494 | } 495 | 496 | function visitArguments(node) { 497 | invariant(node, 'Missing node param'); 498 | isArguments = true; 499 | var nodes = nodesToJSON(node.nodes); 500 | paramsLength += nodes.length; 501 | var text = ''; 502 | nodes.forEach(function (node, idx) { 503 | var prefix = idx ? ', ' : ''; 504 | var nodeText = visitNode(node); 505 | if (node.__type === 'Call') isCallParams = true; 506 | if (GLOBAL_VARIABLE_NAME_LIST.indexOf(nodeText) > -1) nodeText = replaceFirstATSymbol(nodeText); 507 | if (isFunction && !/(^'|")|\d/.test(nodeText) && nodeText) nodeText = replaceFirstATSymbol(nodeText); 508 | text += prefix + nodeText; 509 | paramsLength--; 510 | }); 511 | if (paramsLength === 0) isArguments = false; 512 | return text || ''; 513 | } 514 | 515 | function visitRGBA(node) { 516 | return node.raw.replace(/ /g, ''); 517 | } 518 | 519 | function visitUnit(_ref5) { 520 | var val = _ref5.val, 521 | type = _ref5.type; 522 | 523 | return type ? val + type : val; 524 | } 525 | 526 | function visitBoolean(node) { 527 | return node.val; 528 | } 529 | 530 | function visitIf(node) { 531 | var symbol = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '@if '; 532 | 533 | ifLength++; 534 | invariant(node, 'Missing node param'); 535 | var before = ''; 536 | isIfExpression = true; 537 | if (symbol === '@if ') { 538 | before += handleLinenoAndIndentation(node); 539 | oldLineno = node.lineno; 540 | } 541 | 542 | var condNode = node.cond && node.cond.toJSON() || {}; 543 | isCond = true; 544 | isNegate = node.negate; 545 | var condText = trimSemicolon(visitNode(condNode)); 546 | isCond = false; 547 | isNegate = false; 548 | isIfExpression = false; 549 | var block = visitBlock(node.block); 550 | var elseText = ''; 551 | if (node.elses && node.elses.length) { 552 | var elses = nodesToJSON(node.elses); 553 | elses.forEach(function (node) { 554 | oldLineno++; 555 | if (node.__type === 'If') { 556 | elseText += visitIf(node, ' @else if '); 557 | } else { 558 | elseText += ' @else' + visitBlock(node); 559 | } 560 | }); 561 | } 562 | ifLength--; 563 | return before + symbol + condText + block + elseText; 564 | } 565 | 566 | function visitFunction(node) { 567 | invariant(node, 'Missing node param'); 568 | isFunction = true; 569 | var notMixin = !isFunctionMixin(node.block.nodes); 570 | var before = handleLineno(node.lineno); 571 | oldLineno = node.lineno; 572 | var symbol = ''; 573 | if (notMixin) { 574 | returnSymbol = '@return '; 575 | symbol = '@function'; 576 | } else { 577 | returnSymbol = ''; 578 | symbol = '@mixin'; 579 | } 580 | var params = nodesToJSON(node.params.nodes || []); 581 | FUNCTION_PARAMS = params.map(function (par) { 582 | return par.name; 583 | }); 584 | var paramsText = ''; 585 | params.forEach(function (node, idx) { 586 | var prefix = idx ? ', ' : ''; 587 | var nodeText = visitNode(node); 588 | VARIABLE_NAME_LIST.push(nodeText); 589 | paramsText += prefix + replaceFirstATSymbol(nodeText); 590 | }); 591 | paramsText = paramsText.replace(/\$ +\$/g, '$'); 592 | var fnName = symbol + ' ' + node.name + '(' + trimSemicolon(paramsText) + ')'; 593 | var block = visitBlock(node.block); 594 | returnSymbol = ''; 595 | isFunction = false; 596 | FUNCTION_PARAMS = []; 597 | return before + fnName + block; 598 | } 599 | 600 | function visitTernary(_ref6) { 601 | var cond = _ref6.cond, 602 | lineno = _ref6.lineno; 603 | 604 | var before = handleLineno(lineno); 605 | oldLineno = lineno; 606 | return before + visitBinOp(cond); 607 | } 608 | 609 | function visitBinOp(_ref7) { 610 | var op = _ref7.op, 611 | left = _ref7.left, 612 | right = _ref7.right; 613 | 614 | binOpLength++; 615 | function visitNegate(op) { 616 | if (!isNegate || op !== '==' && op !== '!=') { 617 | return op !== 'is defined' ? op : ''; 618 | } 619 | return op === '==' ? '!=' : '=='; 620 | } 621 | 622 | if (op === '[]') { 623 | var leftText = visitNode(left); 624 | var rightText = visitNode(right); 625 | binOpLength--; 626 | if (isBlock) return 'map-get(' + leftText + ', ' + rightText + ');'; 627 | } 628 | 629 | var leftExp = left ? left.toJSON() : ''; 630 | var rightExp = right ? right.toJSON() : ''; 631 | var isExp = rightExp.__type === 'Expression'; 632 | var expText = isExp ? '(' + visitNode(rightExp) + ')' : visitNode(rightExp); 633 | var symbol = OPEARTION_MAP[op] || visitNegate(op); 634 | var endSymbol = op === 'is defined' ? '!default;' : ''; 635 | 636 | binOpLength--; 637 | return endSymbol ? trimSemicolon(visitNode(leftExp)).trim() + ' ' + endSymbol : visitNode(leftExp) + ' ' + symbol + ' ' + expText; 638 | } 639 | 640 | function visitUnaryOp(_ref8) { 641 | var op = _ref8.op, 642 | expr = _ref8.expr; 643 | 644 | return (OPEARTION_MAP[op] || op) + '(' + visitExpression(expr) + ')'; 645 | } 646 | 647 | function visitEach(node) { 648 | invariant(node, 'Missing node param'); 649 | var before = handleLineno(node.lineno); 650 | oldLineno = node.lineno; 651 | var expr = node.expr && node.expr.toJSON(); 652 | var exprNodes = nodesToJSON(expr.nodes); 653 | var exprText = '@each $' + node.val + ' in '; 654 | VARIABLE_NAME_LIST.push(node.val); 655 | exprNodes.forEach(function (node, idx) { 656 | var prefix = node.__type === 'Ident' ? '$' : ''; 657 | var exp = prefix + visitNode(node); 658 | exprText += idx ? ', ' + exp : exp; 659 | }); 660 | if (/\.\./.test(exprText)) { 661 | exprText = exprText.replace('@each', '@for').replace('..', 'through').replace('in', 'from'); 662 | } 663 | var blank = getIndentation(); 664 | before += blank; 665 | var block = visitBlock(node.block, blank).replace('$' + node.key, ''); 666 | return before + exprText + block; 667 | } 668 | 669 | function visitKeyframes(node) { 670 | isKeyframes = true; 671 | var before = handleLinenoAndIndentation(node); 672 | oldLineno = node.lineno; 673 | var resultText = ''; 674 | var name = visitNodes(node.segments); 675 | var isMixin = !!findNodesType(node.segments, 'Expression'); 676 | var blockJson = node.block.toJSON(); 677 | if (blockJson.nodes.length && blockJson.nodes[0].toJSON().__type === 'Expression') { 678 | throw new Error('Syntax Error Please check if your @keyframes ' + name + ' are correct.'); 679 | } 680 | var block = visitBlock(node.block); 681 | var text = isMixin ? '#{' + name + '}' + block : name + block; 682 | if (autoprefixer) { 683 | KEYFRAMES_LIST.forEach(function (name) { 684 | resultText += before + name + text; 685 | }); 686 | } else { 687 | resultText += before + '@keyframes ' + text; 688 | } 689 | isKeyframes = false; 690 | return resultText; 691 | } 692 | 693 | function visitExtend(node) { 694 | var before = handleLinenoAndIndentation(node); 695 | oldLineno = node.lineno; 696 | var text = visitNodes(node.selectors); 697 | return before + '@extend ' + trimFirst(text) + ';'; 698 | } 699 | 700 | function visitQueryList(node) { 701 | var text = ''; 702 | var nodes = nodesToJSON(node.nodes); 703 | nodes.forEach(function (node, idx) { 704 | var nodeText = visitNode(node); 705 | text += idx ? ', ' + nodeText : nodeText; 706 | }); 707 | return text; 708 | } 709 | 710 | function visitQuery(node) { 711 | var type = visitNode(node.type) || ''; 712 | var nodes = nodesToJSON(node.nodes); 713 | var text = ''; 714 | nodes.forEach(function (node, idx) { 715 | var nodeText = visitNode(node); 716 | text += idx ? ' and ' + nodeText : nodeText; 717 | }); 718 | return type === 'screen' ? type + ' and ' + text : '' + type + text; 719 | } 720 | 721 | function visitMedia(node) { 722 | var before = handleLinenoAndIndentation(node); 723 | oldLineno = node.lineno; 724 | var val = _get(node, ['val'], {}); 725 | var nodeVal = val.toJSON && val.toJSON() || {}; 726 | var valText = visitNode(nodeVal); 727 | var block = visitBlock(node.block); 728 | return before + '@media ' + (valText + block); 729 | } 730 | 731 | function visitFeature(node) { 732 | var segmentsText = visitNodes(node.segments); 733 | var expText = visitExpression(node.expr); 734 | return '(' + segmentsText + ': ' + expText + ')'; 735 | } 736 | 737 | function visitComment(node, isInlineComment) { 738 | var before = isInlineComment ? ' ' : handleLinenoAndIndentation(node); 739 | var matchs = node.str.match(/\n/g); 740 | oldLineno = node.lineno; 741 | if (Array.isArray(matchs)) oldLineno += matchs.length; 742 | var text = node.suppress ? node.str : node.str.replace(/^\/\*/, '/*!'); 743 | return before + text; 744 | } 745 | 746 | function visitMember(_ref9) { 747 | var left = _ref9.left, 748 | right = _ref9.right; 749 | 750 | var searchName = recursiveSearchName(left, 'left', 'name'); 751 | if (searchName && OBJECT_KEY_LIST.indexOf(searchName) > -1) { 752 | return 'map-get(' + visitNode(left) + ', ' + (quote + visitNode(right) + quote) + ')'; 753 | } 754 | return visitNode(left) + '.' + visitNode(right); 755 | } 756 | 757 | function visitAtrule(node) { 758 | var before = handleLinenoAndIndentation(node); 759 | oldLineno = node.lineno; 760 | before += '@' + node.type; 761 | return before + visitBlock(node.block); 762 | } 763 | 764 | function visitObject(_ref10) { 765 | var vals = _ref10.vals, 766 | lineno = _ref10.lineno; 767 | 768 | isObject = true; 769 | indentationLevel++; 770 | var before = repeatString(' ', indentationLevel * 2); 771 | var result = ''; 772 | var count = 0; 773 | for (var key in vals) { 774 | var resultVal = visitNode(vals[key]).replace(/;/, ''); 775 | var symbol = count ? ',' : ''; 776 | result += symbol + '\n' + (before + quote + key + quote) + ': ' + resultVal; 777 | count++; 778 | } 779 | var totalLineno = lineno + count + 2; 780 | oldLineno = totalLineno > oldLineno ? totalLineno : oldLineno; 781 | indentationLevel--; 782 | isObject = false; 783 | return '(' + result + '\n' + repeatString(' ', indentationLevel * 2) + ')'; 784 | } 785 | 786 | function visitCharset(_ref11) { 787 | var _ref11$val = _ref11.val, 788 | value = _ref11$val.val, 789 | quote = _ref11$val.quote, 790 | lineno = _ref11.lineno; 791 | 792 | var before = handleLineno(lineno); 793 | oldLineno = lineno; 794 | return before + '@charset ' + (quote + value + quote) + ';'; 795 | } 796 | 797 | function visitNamespace(_ref12) { 798 | var val = _ref12.val, 799 | lineno = _ref12.lineno; 800 | 801 | isNamespace = true; 802 | var name = '@namespace '; 803 | var before = handleLineno(lineno); 804 | oldLineno = lineno; 805 | if (val.type === 'string') { 806 | var _val$val = val.val, 807 | value = _val$val.val, 808 | valQuote = _val$val.quote; 809 | 810 | isNamespace = false; 811 | return before + name + valQuote + value + valQuote + ';'; 812 | } 813 | return before + name + visitNode(val); 814 | } 815 | 816 | function visitAtrule(_ref13) { 817 | var type = _ref13.type, 818 | block = _ref13.block, 819 | lineno = _ref13.lineno, 820 | segments = _ref13.segments; 821 | 822 | var before = handleLineno(lineno); 823 | oldLineno = lineno; 824 | var typeText = segments.length ? '@' + type + ' ' : '@' + type; 825 | return '' + (before + typeText + visitNodes(segments) + visitBlock(block)); 826 | } 827 | 828 | function visitSupports(_ref14) { 829 | var block = _ref14.block, 830 | lineno = _ref14.lineno, 831 | condition = _ref14.condition; 832 | 833 | var before = handleLineno(lineno); 834 | oldLineno = lineno; 835 | before += getIndentation(); 836 | return before + '@Supports ' + (visitNode(condition) + visitBlock(block)); 837 | } 838 | 839 | function visitString(_ref15) { 840 | var val = _ref15.val, 841 | quote = _ref15.quote; 842 | 843 | return quote + val + quote; 844 | } 845 | 846 | function visitReturn(node) { 847 | if (isFunction) return visitExpression(node.expr).replace(/\n\s*/g, ''); 848 | return '@return $' + visitExpression(node.expr).replace(/\$|\n\s*/g, ''); 849 | } 850 | 851 | // 处理 stylus 语法树;handle stylus Syntax Tree 852 | function visitor(ast, options, globalVariableList, globalMixinList) { 853 | quote = options.quote; 854 | autoprefixer = options.autoprefixer; 855 | GLOBAL_MIXIN_NAME_LIST = globalMixinList; 856 | GLOBAL_VARIABLE_NAME_LIST = globalVariableList; 857 | var result = visitNodes(ast.nodes) || ''; 858 | var indentation = ' '.repeat(options.indentVueStyleBlock); 859 | result = result.replace(/(.*\S.*)/g, indentation + '$1'); 860 | result = result.replace(/(.*)>>>(.*)/g, '$1/deep/$2'); 861 | oldLineno = 1; 862 | FUNCTION_PARAMS = []; 863 | OBJECT_KEY_LIST = []; 864 | PROPERTY_LIST = []; 865 | VARIABLE_NAME_LIST = []; 866 | GLOBAL_MIXIN_NAME_LIST = []; 867 | GLOBAL_VARIABLE_NAME_LIST = []; 868 | return result + '\n'; 869 | } 870 | 871 | function parse(result) { 872 | return new Parser(result).parse(); 873 | } 874 | 875 | function nodeToJSON(data) { 876 | return nodesToJSON(data); 877 | } 878 | 879 | function _get$1(obj, pathArray, defaultValue) { 880 | return _get(obj, pathArray, defaultValue); 881 | } 882 | 883 | function converter(result) { 884 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { 885 | quote: '\'', 886 | conver: 'sass', 887 | autoprefixer: true 888 | }; 889 | var globalVariableList = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : []; 890 | var globalMixinList = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : []; 891 | 892 | if (options.isSignComment) result = result.replace(/\/\/\s(.*)/g, '/* !#sign#! $1 */'); 893 | 894 | // Add semicolons to properties with inline comments to ensure that they are parsed correctly 895 | result = result.replace(/^( *)(\S(.+?))( *)(\/\*.*\*\/)$/gm, '$1$2;$4$5'); 896 | 897 | if (typeof result !== 'string') return result; 898 | var ast = new Parser(result).parse(); 899 | // 开发时查看 ast 对象。 900 | // console.log(JSON.stringify(ast)) 901 | var text = visitor(ast, options, globalVariableList, globalMixinList); 902 | // Convert special multiline comments to single-line comments 903 | return text.replace(/\/\*\s!#sign#!\s(.*)\s\*\//g, '// $1'); 904 | } 905 | 906 | exports.parse = parse; 907 | exports.nodeToJSON = nodeToJSON; 908 | exports._get = _get$1; 909 | exports.converter = converter; 910 | --------------------------------------------------------------------------------