@zohodesk/react-cli 0.0.1-exp.167.1 → 0.0.1-exp.167.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. package/.eslintrc.js +1 -0
  2. package/.prettierrc +6 -0
  3. package/README.md +98 -3
  4. package/bin/cli.js +10 -20
  5. package/docs/HoverActive.md +12 -0
  6. package/docs/InstallNode.md +28 -0
  7. package/docs/VariableConversion.md +678 -0
  8. package/lib/common/splitChunks.js +133 -7
  9. package/lib/configs/jest.config.js +8 -12
  10. package/lib/configs/libAlias.js +10 -3
  11. package/lib/configs/webpack.dev.config.js +21 -26
  12. package/lib/configs/webpack.docs.config.js +13 -19
  13. package/lib/configs/webpack.impact.config.js +13 -19
  14. package/lib/configs/webpack.prod.config.js +19 -15
  15. package/lib/jest/preProcessors/cssPreprocessor.js +16 -7
  16. package/lib/loaderUtils/configsAssetsLoaders.js +117 -0
  17. package/lib/loaderUtils/getCSSLoaders.js +51 -7
  18. package/lib/plugins/I18nSplitPlugin/I18nSplit.md +63 -54
  19. package/lib/postcss-plugins/{ExcludeRTLPlugin.js → ExcludePlugin.js} +1 -1
  20. package/lib/postcss-plugins/__test__/hoverActivePlugin.spec.js +22 -0
  21. package/lib/postcss-plugins/__test__/test1Input.css +39 -0
  22. package/lib/postcss-plugins/__test__/test1Output.css +39 -0
  23. package/lib/postcss-plugins/hoverActivePlugin.js +385 -0
  24. package/lib/postcss-plugins/variableModificationPlugin/ErrorHandler.js +37 -0
  25. package/lib/postcss-plugins/variableModificationPlugin/index.js +247 -0
  26. package/lib/postcss-plugins/variableModifier.js +244 -0
  27. package/lib/schemas/index.js +57 -6
  28. package/lib/servers/getCliPath.js +7 -3
  29. package/lib/utils/getOptions.js +29 -1
  30. package/package.json +4 -3
  31. package/cert/cert.pem +0 -37
  32. package/cert/key.pem +0 -27
  33. package/cert/passphrase.pem +0 -1
  34. package/eslint/NOTES.md +0 -3
  35. package/eslint/React_CDN.zip +0 -0
  36. package/eslint/a.sh +0 -14
  37. package/eslint/a23.c +0 -16
  38. package/eslint/a28.c +0 -25
  39. package/eslint/a29.c +0 -25
  40. package/eslint/a30.c +0 -29
  41. package/eslint/a31.c +0 -23
  42. package/eslint/a35.c +0 -23
  43. package/eslint/a36.c +0 -18
  44. package/eslint/a37.c +0 -25
  45. package/eslint/a38.c +0 -28
  46. package/eslint/a39.c +0 -17
  47. package/eslint/a40.c +0 -32
  48. package/eslint/mockapi.html +0 -18
  49. package/eslint/mockapi.md +0 -5
  50. package/eslint/testa/build.sh +0 -7
  51. package/eslint/testa/build1/index.html +0 -12
  52. package/eslint/testa/build1/js/2_.js +0 -15
  53. package/eslint/testa/build1/js/2_.js.map +0 -1
  54. package/eslint/testa/build1/js/main_.js +0 -40
  55. package/eslint/testa/build1/js/main_.js.map +0 -1
  56. package/eslint/testa/build1/js/runtime~main_.js +0 -251
  57. package/eslint/testa/build1/js/runtime~main_.js.map +0 -1
  58. package/eslint/testa/build2/index.html +0 -12
  59. package/eslint/testa/build2/js/2_.js +0 -15
  60. package/eslint/testa/build2/js/2_.js.map +0 -1
  61. package/eslint/testa/build2/js/3_.js +0 -15
  62. package/eslint/testa/build2/js/3_.js.map +0 -1
  63. package/eslint/testa/build2/js/main_.js +0 -46
  64. package/eslint/testa/build2/js/main_.js.map +0 -1
  65. package/eslint/testa/build2/js/runtime~main_.js +0 -251
  66. package/eslint/testa/build2/js/runtime~main_.js.map +0 -1
  67. package/eslint/testa/build3/index.710b00fba04c6c594ad3.html +0 -12
  68. package/eslint/testa/build3/js/2.321b867f0966f9c9cdfd_.js +0 -1
  69. package/eslint/testa/build3/js/main.eb2aec4c9f1c16a385e0_.js +0 -1
  70. package/eslint/testa/build3/js/runtime~main.fafbbe7484e9c126f4f7_.js +0 -1
  71. package/eslint/testa/build3/manifest.json +0 -1
  72. package/eslint/testa/build4/index.9ff03a2ccdc9b904f1fe.html +0 -12
  73. package/eslint/testa/build4/js/2.8b63ce57af6dd2bac274_.js +0 -1
  74. package/eslint/testa/build4/js/3.5208acbe37a44362090e_.js +0 -1
  75. package/eslint/testa/build4/js/main.a934d6c2e2329d97269e_.js +0 -1
  76. package/eslint/testa/build4/js/runtime~main.5b85dd9ab73069c1455a_.js +0 -1
  77. package/eslint/testa/build4/manifest.json +0 -1
  78. package/eslint/testa/package.json +0 -24
  79. package/eslint/testa/scr/chunk1.js +0 -3
  80. package/eslint/testa/scr/chunk2.js +0 -3
  81. package/eslint/testa/scr/index.html +0 -12
  82. package/eslint/testa/scr/index.js +0 -4
  83. package/eslint/testa/scr/utlis.js +0 -12
  84. package/eslint/testa/src/chunk1.js +0 -3
  85. package/eslint/testa/src/index.html +0 -12
  86. package/eslint/testa/src/index.js +0 -3
  87. package/eslint/testa/src/utlis.js +0 -7
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.configAudioLoader = configAudioLoader;
7
+ exports.configFontLoader = configFontLoader;
8
+ exports.configImageLoader = configImageLoader;
9
+ exports.configSVGLoader = configSVGLoader;
10
+ exports.configVideoLoader = configVideoLoader;
11
+ exports.configVideoLoaderObj = configVideoLoaderObj;
12
+ exports.createNameTemplate = createNameTemplate;
13
+
14
+ var _path = _interopRequireDefault(require("path"));
15
+
16
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
17
+
18
+ // function getLoaderOptionQueryString(params) {
19
+ const ImageExtRegex = /\.jpe?g$|\.gif$|\.png$/;
20
+ const FontExtRegex = /\.woff2|\.woff$|\.ttf$|\.eot$/;
21
+ const SVGExtRegex = /\.svg$/;
22
+ const AudioExtRegex = /\.ogg$/;
23
+ const VidioExtRegex = /\.mp4$/;
24
+
25
+ function createLoaderOptionQueryString(loaderName, nameTemplate, limit = 1000) {
26
+ return `${loaderName}?limit=${limit}&name=${nameTemplate}`;
27
+ }
28
+
29
+ function configImageLoader(nameTemplate) {
30
+ return {
31
+ test: ImageExtRegex,
32
+ use: createLoaderOptionQueryString('url-loader', `./images/${nameTemplate}`)
33
+ };
34
+ }
35
+
36
+ function configFontLoader(nameTemplate) {
37
+ return {
38
+ test: FontExtRegex,
39
+ use: createLoaderOptionQueryString('url-loader', `./fonts/${nameTemplate}`)
40
+ };
41
+ }
42
+
43
+ function configSVGLoader(nameTemplate) {
44
+ return {
45
+ test: SVGExtRegex,
46
+ use: createLoaderOptionQueryString('url-loader', `./fonts/${nameTemplate}`, 1)
47
+ };
48
+ }
49
+
50
+ function configAudioLoader(nameTemplate) {
51
+ return {
52
+ test: AudioExtRegex,
53
+ use: createLoaderOptionQueryString('file-loader', `./fonts/${nameTemplate}`, 1)
54
+ };
55
+ }
56
+
57
+ function configVideoLoader(nameTemplate) {
58
+ return {
59
+ test: VidioExtRegex,
60
+ use: createLoaderOptionQueryString('url-loader', `./images/${nameTemplate}`, 1)
61
+ };
62
+ }
63
+
64
+ function configVideoLoaderObj(nameTemplate) {
65
+ return {
66
+ test: VidioExtRegex,
67
+ use: {
68
+ loader: 'url-loader',
69
+ options: {
70
+ limit: 1000,
71
+ name: nameTemplate,
72
+ fallback: _path.default.join(__dirname, '..', 'loaders', 'fileLoader.js')
73
+ }
74
+ }
75
+ };
76
+ }
77
+
78
+ function createNameTemplate(enableChunkHash) {
79
+ const ext = `${enableChunkHash ? '.[hash:20]' : ''}.[ext]`;
80
+ return `[name]${ext}`;
81
+ }
82
+ /*
83
+ export function createImageAndFontsAndSVGLoaders(enableChunkHash) {
84
+ const nameTemplate = createNameTemplate(enableChunkHash);
85
+ return [
86
+ configImageLoader(nameTemplate),
87
+ configFontLoader(nameTemplate),
88
+ configSVGLoader(nameTemplate),
89
+ configAudioLoader(nameTemplate)
90
+ ];
91
+ }
92
+ */
93
+
94
+ /*
95
+ export function createLoaderOptionObject(
96
+ loaderName,
97
+ nameTemplate,
98
+ fallback,
99
+ limit = 1000
100
+ ) {
101
+ return {
102
+ loader: loaderName,
103
+ options: {
104
+ limit,
105
+ name: nameTemplate,
106
+ fallback
107
+ }
108
+ };
109
+ }
110
+
111
+ function configLoaderObject(filter, loaderAndOptions) {
112
+ return {
113
+ test: filter,
114
+ use: loaderAndOptions
115
+ };
116
+ }
117
+ */
@@ -13,10 +13,31 @@ var _utils = require("../utils");
13
13
 
14
14
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
15
15
 
16
- let options = (0, _utils.getOptions)();
17
- let isWin = process.platform === 'win32';
16
+ const fs = require('fs');
18
17
 
19
- let getCSSLoaders = (hasRTL, rtlExclude, classNameBlob, cssUniqueness, selectorReplace, cssHashSelectors, classNamePrefix) => {
18
+ const options = (0, _utils.getOptions)();
19
+ const isWin = process.platform === 'win32';
20
+
21
+ function windowsModification(array) {
22
+ return isWin ? array.map(r => r.replace(/\//g, '\\')) : array;
23
+ }
24
+
25
+ function excludeEmptyCheckPlugin({
26
+ enable,
27
+ ignore,
28
+ plugins
29
+ }) {
30
+ return enable ? ignore.length === 0 ? plugins : [require('../postcss-plugins/ExcludePlugin')({
31
+ ignore,
32
+ plugins
33
+ })] : [];
34
+ }
35
+
36
+ const getCSSLoaders = (plugins, exclude, mediaQueryHoverActiveString, cssVariableReplacementConfig, classNameBlob, cssUniqueness, selectorReplace, cssHashSelectors, classNamePrefix) => {
37
+ // console.log('plugins:')
38
+ // console.log(plugins)
39
+ // console.log('exclude:')
40
+ // console.log(exclude)
20
41
  const {
21
42
  devCssFileBountry
22
43
  } = options.app;
@@ -26,9 +47,14 @@ let getCSSLoaders = (hasRTL, rtlExclude, classNameBlob, cssUniqueness, selectorR
26
47
  const {
27
48
  cssSelectorZipPath
28
49
  } = options.impactService;
29
- let rtlExcludeLocal = isWin ? rtlExclude.map(r => r.replace(/\//g, '\\')) : rtlExclude;
30
- let cssLoaderOptions = {
31
- importLoaders: hasRTL ? 1 : 0,
50
+ fs.writeFileSync('./css_error.log', '');
51
+ const rtlExcludeLocal = windowsModification(exclude.rtl);
52
+ const hoverActiveExcludeLocal = windowsModification(exclude.hoverActive);
53
+ const combinerMediaQueryExcludeLocal = windowsModification(exclude.combinerMediaQuery);
54
+ const cssVariableReplacementExcludeLocal = windowsModification(exclude.cssVariableReplacement);
55
+ const cssLoaderOptions = {
56
+ // importLoaders: hasRTL||hoverActive ? 1 : 0,
57
+ importLoaders: 1,
32
58
  modules: {},
33
59
  sourceMap: true
34
60
  };
@@ -39,7 +65,13 @@ let getCSSLoaders = (hasRTL, rtlExclude, classNameBlob, cssUniqueness, selectorR
39
65
  cssLoaderOptions.modules.getLocalIdent = (0, _cssClassNameGenerate.default)(cssUniqueness, cssHashSelectors, classNamePrefix);
40
66
  }
41
67
 
42
- const postcssPlugins = [valueReplacer && require('../postcss-plugins/ValueReplacer')(valueReplacer), selectorReplace && require('postcss-selector-replace')(selectorReplace), hasRTL && require('../postcss-plugins/ExcludeRTLPlugin')({
68
+ const postcssPlugins = [valueReplacer && require('../postcss-plugins/ValueReplacer')(valueReplacer), selectorReplace && require('postcss-selector-replace')(selectorReplace), // ...excludeEmptyCheckPlugin({
69
+ // enable: true,
70
+ // ignore: [],
71
+ // plugins: [require('../postcss-plugins/variablePropertiesCollector').default()]
72
+ // }),
73
+ ...excludeEmptyCheckPlugin({
74
+ enable: plugins.hasRTL,
43
75
  ignore: rtlExcludeLocal,
44
76
  plugins: [require('@zohodesk/postcss-rtl')({
45
77
  addPrefixToSelector: function addPrefixToSelector(selector, prefix) {
@@ -50,6 +82,18 @@ let getCSSLoaders = (hasRTL, rtlExclude, classNameBlob, cssUniqueness, selectorR
50
82
  return `${prefix} ${selector}`; // Make selectors like [dir=rtl] > .selector
51
83
  }
52
84
  })]
85
+ }), ...excludeEmptyCheckPlugin({
86
+ enable: plugins.combinerMediaQuery,
87
+ ignore: combinerMediaQueryExcludeLocal,
88
+ plugins: [require('postcss-combine-media-query')]
89
+ }), ...excludeEmptyCheckPlugin({
90
+ enable: plugins.hoverActive,
91
+ ignore: hoverActiveExcludeLocal,
92
+ plugins: [require('../postcss-plugins/hoverActivePlugin')(mediaQueryHoverActiveString)]
93
+ }), ...excludeEmptyCheckPlugin({
94
+ enable: plugins.cssVariableReplacement,
95
+ ignore: cssVariableReplacementExcludeLocal,
96
+ plugins: [fs.existsSync(cssVariableReplacementConfig) && require('../postcss-plugins/variableModificationPlugin/index')(cssVariableReplacementConfig)]
53
97
  })].filter(Boolean);
54
98
  return [cssSelectorZipPath && {
55
99
  loader: require.resolve('../loaders/selectorMappingLoader')
@@ -3,84 +3,93 @@
3
3
  generaly we have manage our all I18n keys and values as language specific i18n files and then we download all i18n in initial page load.
4
4
 
5
5
  ### what is the problem with this?.
6
+
6
7
  the problem is i18n file keep get's grown it will affect the the inital page load and critial rendering path.
7
8
  So, We have decide to create a plugin for split i18n per chunk's of js vise.
8
9
 
9
10
  ### what is i18n split?
10
- it is like code split for i18n.
11
- we will collect i18n from js chunk and we will create separate i18n chunk per js chunk,
12
- with this we already load Js chunks are on demand and now we also download i18n also ondemand with this the initial and forthere actions.
13
- when js download's we will download i18n with it.
14
11
 
15
- ### How to we going to i18n split?
16
- we will read the js chunk and collect I18n keys form it,
17
- then we will create i18n chunk files with these used keys.
12
+ it is like code split for i18n.
13
+ we will collect i18n from js chunk and we will create separate i18n chunk per js chunk,
14
+ with this we already load Js chunks are on demand and now we also download i18n also ondemand with this the initial and forthere actions.
15
+ when js download's we will download i18n with it.
16
+
17
+ ### How to we going to i18n split?
18
+
19
+ we will read the js chunk and collect I18n keys form it,
20
+ then we will create i18n chunk files with these used keys.
18
21
 
19
22
  ### how do you collect i18n keys from js files?
20
- we will traverse the js file's AST (static analysis) and find i18 keys,
21
- we will concider all string which is in jsResource file as i18n keys.
22
- and you can tell dynamic i18n key by js comments,
23
- Like Below:-
24
- ```js
25
- fetch("tickets?view=view1").then(data => {
26
- // I18n support.ticketsEmpty
27
- // I18n support.viewNotFount
28
- // I18n support.permissionDenied
29
- let text = getI18nValue(data.i18nkey);
30
- })
31
- ```
23
+
24
+ we will traverse the js file's AST (static analysis) and find i18 keys,
25
+ we will concider all string which is in jsResource file as i18n keys.
26
+ and you can tell dynamic i18n key by js comments,
27
+ Like Below:-
28
+
29
+ ```js
30
+ fetch('tickets?view=view1').then(data => {
31
+ // I18n support.ticketsEmpty
32
+ // I18n support.viewNotFount
33
+ // I18n support.permissionDenied
34
+ let text = getI18nValue(data.i18nkey);
35
+ });
36
+ ```
32
37
 
33
38
  ### is there any posiblity for unwanted keys in some chunk?
34
- Yes, there is two posiblity
35
- 1. js comment , if you write js comment but it is not needed then it will be add into i18n chunk
36
- `To Resolve` we currently do not have perfect idea for this, But we can check this by chunk size limit.
37
- `idea's will be welcome`
38
- 2. like we said "we will concider all string which is in jsResource file as i18n keys",
39
- So if you use string not for i18n but it was in jsResource then it will be add into i18n chunk.
40
- `To Resolve` this problem we can use some kinda prefix or something.
39
+
40
+ Yes, there is two posiblity
41
+
42
+ 1. js comment , if you write js comment but it is not needed then it will be add into i18n chunk
43
+ `To Resolve` we currently do not have perfect idea for this, But we can check this by chunk size limit.
44
+ `idea's will be welcome`
45
+ 2. like we said "we will concider all string which is in jsResource file as i18n keys",
46
+ So if you use string not for i18n but it was in jsResource then it will be add into i18n chunk.
47
+ `To Resolve` this problem we can use some kinda prefix or something.
41
48
 
42
49
  ### how do we downlowd and give to app?
43
- we have over write defualt webpack requireEnsure (every dynamic chunk requests are done by that function) function.
44
- and we parlarly dowload i18n files. and we ask jsonFunction, to our plugin.
45
- So we send i18nkeys , when it was download,
46
- you must store all i18n keys, we give asycrnsly by that function download in that patticular.
50
+
51
+ we have over write defualt webpack requireEnsure (every dynamic chunk requests are done by that function) function.
52
+ and we parlarly dowload i18n files. and we ask jsonFunction, to our plugin.
53
+ So we send i18nkeys , when it was download,
54
+ you must store all i18n keys, we give asycrnsly by that function download in that patticular.
47
55
  so you must store and update (like append or assign) everytime that function call.
48
56
 
49
57
  ### how to use this feature?
50
- to use this feature use have give the below oprtions
51
- `package.json`
52
- ```json
53
- {
54
- /// ...some things
55
- "react-cli": {
56
- // ...some things
57
- "i18n": {
58
- "chunkSplitEnable": true,
59
- "jsResource": "./deskapp/properties/JSResources.properties",
60
- "localeVarName": "window.userInfo.langCode",
61
- "jsonpFunc": "window.loadI18nChunk",
62
- "templateLabel": "{{--user-locale}}",// this is for html template i18n file path locate template
63
- "propertiesFolder": "./deskapp/properties"
64
- },
65
- // ...some things
58
+
59
+ to use this feature use have give the below oprtions
60
+ `package.json`
61
+
62
+ ```json
63
+ {
64
+ /// ...some things
65
+ "react-cli": {
66
+ // ...some things
67
+ "i18n": {
68
+ "chunkSplitEnable": true,
69
+ "jsResource": "./deskapp/properties/JSResources.properties",
70
+ "localeVarName": "window.userInfo.langCode",
71
+ "jsonpFunc": "window.loadI18nChunk",
72
+ "templateLabel": "{{--user-locale}}", // this is for html template i18n file path locate template
73
+ "propertiesFolder": "./deskapp/properties"
66
74
  }
75
+ // ...some things
67
76
  }
68
- ```
69
-
77
+ }
78
+ ```
70
79
 
71
80
  <!-- not need below -->
72
- <!--
81
+ <!--we have added new feature for css to write logic to how hover related css work in hoverhover and a
73
82
 
74
83
  ### is there any problems with static analysis?
75
- Yes,
84
+ Yes,
76
85
 
77
86
  ### how do we use dynamic i18n key?
78
87
 
79
- and we will download i18n file with
88
+ and we will download i18n file with
80
89
 
81
90
  and when js chunk download's then we will download i18n as need.
82
91
  we all know in our app we loading i18n as one single big file for each locale(language).
83
- we are going to split i18n as per js chunk, and when js chunk download's then we will download i18n as need.
84
-
85
-
92
+ we are going to split i18n as per js chunk, and when js chunk download's then we will download i18n as need.
93
+
94
+
86
95
  -->
@@ -4,7 +4,7 @@ var _postcss = _interopRequireDefault(require("postcss"));
4
4
 
5
5
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
6
6
 
7
- module.exports = _postcss.default.plugin('postcss-exclude-rtl-files', opts => {
7
+ module.exports = _postcss.default.plugin('postcss-exclude-files', opts => {
8
8
  const {
9
9
  plugins
10
10
  } = opts;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+
3
+ const fs = require('fs');
4
+
5
+ const postcss = require('postcss');
6
+
7
+ function expectRunPostCss(inputFile, outputfile, cb) {
8
+ const inputStr = fs.readFileSync(inputFile, 'utf-8');
9
+ postcss(plugins).process(inputStr, {
10
+ from,
11
+ to
12
+ }).then(result => {
13
+ expect(result.css).toBe(fs.readFileSync(outputfile, 'utf-8'));
14
+ cb();
15
+ });
16
+ }
17
+
18
+ describe('To Check Hover active postcss Plugin ', () => {
19
+ test('should handle normal rule hover', cb => {
20
+ expectRunPostCss('test1Input.css', 'test1Output.css', cb);
21
+ });
22
+ });
@@ -0,0 +1,39 @@
1
+ /*Hover_active:ignore*/
2
+ g,a:hover{
3
+ color : red
4
+ }
5
+ /*Hover:ignore*/
6
+ h:hover{
7
+ background : yellow
8
+ }
9
+
10
+ /* Hover_active:ignore */
11
+ g,d+e:hover{
12
+ color : black
13
+ }
14
+
15
+ g,d e:hover{
16
+ color : black
17
+ }
18
+
19
+ @media screen and (max-width:61.25em){
20
+ /* Hover_active:ignore */
21
+ a,b,a:hover, b:hover{
22
+ background-color : blue
23
+ }
24
+ a + b,a:hover + b:hover{
25
+ background-color : blue
26
+ }
27
+ a b:hover{
28
+ background-color : blue
29
+ }
30
+
31
+ .cc:hover {
32
+ color: red;
33
+ }
34
+
35
+ c:hover{
36
+ color : red
37
+ }
38
+
39
+ }
@@ -0,0 +1,39 @@
1
+ /*Hover_active:ignore*/
2
+ g,a:hover{
3
+ color : red
4
+ }
5
+ /*Hover:ignore*/
6
+ h:hover{
7
+ background : yellow
8
+ }
9
+
10
+ /* Hover_active:ignore */
11
+ g,d+e:hover{
12
+ color : black
13
+ }
14
+
15
+ g,d e:hover{
16
+ color : black
17
+ }
18
+
19
+ @media screen and (max-width:61.25em){
20
+ /* Hover_active:ignore */
21
+ a,b,a:hover, b:hover{
22
+ background-color : blue
23
+ }
24
+ a + b,a:hover + b:hover{
25
+ background-color : blue
26
+ }
27
+ a b:hover{
28
+ background-color : blue
29
+ }
30
+
31
+ .cc:hover {
32
+ color: red;
33
+ }
34
+
35
+ c:hover{
36
+ color : red
37
+ }
38
+
39
+ }