├── 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 |
2 |
3 |
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 |
2 |
3 |
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 |
2 |
3 |
4 |
5 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/test/vue/scss/basic.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
14 |
--------------------------------------------------------------------------------
/test/vue/stylus/indented.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/test/vue/stylus/scoped.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/test/vue/scss/scoped.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
14 |
--------------------------------------------------------------------------------
/test/vue/scss/indented.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
14 |
--------------------------------------------------------------------------------
/test/vue/stylus/deep.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
2 |
3 |
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 |
2 |
3 |
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 |
2 |
3 |
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 = /`;
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 |
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 |
156 |
157 | click me
158 |
159 |
160 |
161 |
166 | ```
167 |
168 | ### 转换后的 `.vue` 文件
169 | ```html
170 |
171 |
172 | click me
173 |
174 |
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 |
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 |
157 |
158 | click me
159 |
160 |
161 |
162 |
167 | ```
168 |
169 | ### Converted `.vue` file
170 |
171 | ```html
172 |
173 |
174 | click me
175 |
176 |
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 |
--------------------------------------------------------------------------------