@mpxjs/webpack-plugin 2.10.17-unocss.1 → 2.10.18-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +49 -36
- package/lib/platform/style/wx/index.js +0 -2
- package/lib/platform/template/wx/component-config/camera.js +12 -0
- package/lib/platform/template/wx/component-config/unsupported.js +1 -1
- package/lib/react/processStyles.js +8 -39
- package/lib/react/style-helper.js +5 -16
- package/lib/resolver/ExtendComponentsPlugin.js +60 -0
- package/lib/runtime/components/ali/mpx-section-list.mpx +566 -0
- package/lib/runtime/components/ali/mpx-sticky-header.mpx +212 -0
- package/lib/runtime/components/ali/mpx-sticky-section.mpx +17 -0
- package/lib/runtime/components/react/dist/animationHooks/useAnimationAPIHooks.js +0 -1
- package/lib/runtime/components/react/dist/mpx-async-suspense.jsx +2 -1
- package/lib/runtime/components/react/dist/mpx-input.jsx +10 -3
- package/lib/runtime/components/react/dist/mpx-keyboard-avoiding-view.jsx +13 -2
- package/lib/runtime/components/react/dist/mpx-swiper.jsx +60 -40
- package/lib/runtime/components/react/dist/utils.jsx +8 -4
- package/lib/runtime/components/react/mpx-async-suspense.tsx +2 -1
- package/lib/runtime/components/react/mpx-camera.tsx +327 -0
- package/lib/runtime/components/react/mpx-input.tsx +11 -2
- package/lib/runtime/components/react/mpx-keyboard-avoiding-view.tsx +13 -2
- package/lib/runtime/components/react/mpx-section-list.tsx +439 -0
- package/lib/runtime/components/react/mpx-swiper.tsx +113 -66
- package/lib/runtime/components/react/mpx-web-view.tsx +1 -1
- package/lib/runtime/components/react/tsconfig.json +1 -1
- package/lib/runtime/components/react/utils.tsx +8 -4
- package/lib/runtime/components/web/mpx-scroll-view.vue +5 -2
- package/lib/runtime/components/web/mpx-section-list.vue +551 -0
- package/lib/runtime/components/wx/mpx-section-list-default/list-footer.mpx +26 -0
- package/lib/runtime/components/wx/mpx-section-list-default/list-header.mpx +26 -0
- package/lib/runtime/components/wx/mpx-section-list-default/list-item.mpx +26 -0
- package/lib/runtime/components/wx/mpx-section-list-default/section-header.mpx +26 -0
- package/lib/runtime/components/wx/mpx-section-list.mpx +209 -0
- package/lib/runtime/components/wx/mpx-sticky-header.mpx +40 -0
- package/lib/runtime/components/wx/mpx-sticky-section.mpx +31 -0
- package/lib/template-compiler/compiler.js +7 -3
- package/lib/utils/const.js +29 -0
- package/lib/utils/dom-tag-config.js +1 -1
- package/lib/wxss/loader.js +4 -1
- package/lib/wxss/utils.js +7 -2
- package/package.json +3 -3
package/lib/index.js
CHANGED
|
@@ -30,6 +30,7 @@ const AddEnvPlugin = require('./resolver/AddEnvPlugin')
|
|
|
30
30
|
const PackageEntryPlugin = require('./resolver/PackageEntryPlugin')
|
|
31
31
|
const DynamicRuntimePlugin = require('./resolver/DynamicRuntimePlugin')
|
|
32
32
|
const FixDescriptionInfoPlugin = require('./resolver/FixDescriptionInfoPlugin')
|
|
33
|
+
const ExtendComponentsPlugin = require('./resolver/ExtendComponentsPlugin')
|
|
33
34
|
// const CommonJsRequireDependency = require('webpack/lib/dependencies/CommonJsRequireDependency')
|
|
34
35
|
// const HarmonyImportSideEffectDependency = require('webpack/lib/dependencies/HarmonyImportSideEffectDependency')
|
|
35
36
|
// const RequireHeaderDependency = require('webpack/lib/dependencies/RequireHeaderDependency')
|
|
@@ -403,6 +404,7 @@ class MpxWebpackPlugin {
|
|
|
403
404
|
const addEnvPlugin = new AddEnvPlugin('before-file', this.options.env, this.options.fileConditionRules, 'file')
|
|
404
405
|
const packageEntryPlugin = new PackageEntryPlugin('before-file', this.options.miniNpmPackages, this.options.normalNpmPackages, 'file')
|
|
405
406
|
const dynamicPlugin = new DynamicPlugin('result', this.options.dynamicComponentRules)
|
|
407
|
+
const extendComponentsPlugin = new ExtendComponentsPlugin('described-resolve', this.options.mode, 'resolve')
|
|
406
408
|
|
|
407
409
|
if (Array.isArray(compiler.options.resolve.plugins)) {
|
|
408
410
|
compiler.options.resolve.plugins.push(addModePlugin)
|
|
@@ -417,6 +419,7 @@ class MpxWebpackPlugin {
|
|
|
417
419
|
}
|
|
418
420
|
compiler.options.resolve.plugins.push(packageEntryPlugin)
|
|
419
421
|
compiler.options.resolve.plugins.push(new FixDescriptionInfoPlugin())
|
|
422
|
+
compiler.options.resolve.plugins.push(extendComponentsPlugin)
|
|
420
423
|
compiler.options.resolve.plugins.push(dynamicPlugin)
|
|
421
424
|
|
|
422
425
|
const optimization = compiler.options.optimization
|
|
@@ -1455,6 +1458,7 @@ class MpxWebpackPlugin {
|
|
|
1455
1458
|
const context = parser.state.module.context
|
|
1456
1459
|
const { queryObj, resourcePath } = parseRequest(request)
|
|
1457
1460
|
let tarRoot = queryObj.root
|
|
1461
|
+
|
|
1458
1462
|
if (!tarRoot && mpx.asyncSubpackageRules) {
|
|
1459
1463
|
for (const item of mpx.asyncSubpackageRules) {
|
|
1460
1464
|
if (matchCondition(resourcePath, item)) {
|
|
@@ -1463,49 +1467,49 @@ class MpxWebpackPlugin {
|
|
|
1463
1467
|
}
|
|
1464
1468
|
}
|
|
1465
1469
|
}
|
|
1466
|
-
|
|
1470
|
+
// TODO 后续考虑和 asyncSubpackageRules 配置合并
|
|
1471
|
+
if (isReact(mpx.mode)) tarRoot = transSubpackage(mpx.transSubpackageRules, tarRoot)
|
|
1472
|
+
|
|
1473
|
+
if (tarRoot && mpx.supportRequireAsync) {
|
|
1467
1474
|
// 删除root query
|
|
1468
1475
|
if (queryObj.root) request = addQuery(request, {}, false, ['root'])
|
|
1469
1476
|
// wx、ali和web平台支持require.async,其余平台使用CommonJsAsyncDependency进行模拟抹平
|
|
1470
|
-
if (mpx.
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
depBlock.addDependency(dep)
|
|
1486
|
-
parser.state.current.addBlock(depBlock)
|
|
1487
|
-
} else {
|
|
1488
|
-
const dep = new DynamicEntryDependency(range, request, 'export', '', tarRoot, '', context, {
|
|
1489
|
-
isAsync: true,
|
|
1490
|
-
isRequireAsync: true,
|
|
1491
|
-
retryRequireAsync: this.options.retryRequireAsync,
|
|
1492
|
-
requireAsyncRange: expr.range
|
|
1493
|
-
})
|
|
1494
|
-
|
|
1495
|
-
parser.state.current.addPresentationalDependency(dep)
|
|
1496
|
-
// 包含require.async的模块不能被concatenate,避免DynamicEntryDependency中无法获取模块chunk以计算相对路径
|
|
1497
|
-
parser.state.module.buildInfo.moduleConcatenationBailout = 'require async'
|
|
1498
|
-
}
|
|
1477
|
+
if (isWeb(mpx.mode) || isReact(mpx.mode)) {
|
|
1478
|
+
const depBlock = new AsyncDependenciesBlock(
|
|
1479
|
+
{
|
|
1480
|
+
name: tarRoot + '/index'
|
|
1481
|
+
},
|
|
1482
|
+
expr.loc,
|
|
1483
|
+
request
|
|
1484
|
+
)
|
|
1485
|
+
const dep = new ImportDependency(request, expr.range, undefined, {
|
|
1486
|
+
isRequireAsync: true,
|
|
1487
|
+
retryRequireAsync: this.options.retryRequireAsync
|
|
1488
|
+
})
|
|
1489
|
+
dep.loc = expr.loc
|
|
1490
|
+
depBlock.addDependency(dep)
|
|
1491
|
+
parser.state.current.addBlock(depBlock)
|
|
1499
1492
|
} else {
|
|
1500
|
-
const
|
|
1501
|
-
|
|
1502
|
-
|
|
1493
|
+
const dep = new DynamicEntryDependency(range, request, 'export', '', tarRoot, '', context, {
|
|
1494
|
+
isAsync: true,
|
|
1495
|
+
isRequireAsync: true,
|
|
1496
|
+
retryRequireAsync: this.options.retryRequireAsync,
|
|
1497
|
+
requireAsyncRange: expr.range
|
|
1498
|
+
})
|
|
1499
|
+
|
|
1500
|
+
parser.state.current.addPresentationalDependency(dep)
|
|
1501
|
+
// 包含require.async的模块不能被concatenate,避免DynamicEntryDependency中无法获取模块chunk以计算相对路径
|
|
1502
|
+
parser.state.module.buildInfo.moduleConcatenationBailout = 'require async'
|
|
1503
1503
|
}
|
|
1504
|
-
if (args) parser.walkExpressions(args)
|
|
1505
|
-
return true
|
|
1506
1504
|
} else {
|
|
1507
|
-
|
|
1505
|
+
const dep = new CommonJsAsyncDependency(request, expr.range)
|
|
1506
|
+
parser.state.current.addDependency(dep)
|
|
1507
|
+
if (!tarRoot) {
|
|
1508
|
+
compilation.warnings.push(new Error(`The require async JS [${request}] need to declare subpackage name by root`))
|
|
1509
|
+
}
|
|
1508
1510
|
}
|
|
1511
|
+
if (args) parser.walkExpressions(args)
|
|
1512
|
+
return true
|
|
1509
1513
|
}
|
|
1510
1514
|
}
|
|
1511
1515
|
|
|
@@ -2071,3 +2075,12 @@ try {
|
|
|
2071
2075
|
}
|
|
2072
2076
|
|
|
2073
2077
|
module.exports = MpxWebpackPlugin
|
|
2078
|
+
|
|
2079
|
+
/**
|
|
2080
|
+
* 定义 MpxWebpackPlugin 的配置
|
|
2081
|
+
* @param {MpxWebpackPluginOptions} options - 插件选项
|
|
2082
|
+
* @returns {MpxWebpackPluginOptions}
|
|
2083
|
+
*/
|
|
2084
|
+
module.exports.defineConfig = function defineConfig(options) {
|
|
2085
|
+
return options
|
|
2086
|
+
}
|
|
@@ -193,7 +193,6 @@ module.exports = function getSpec({ warn, error }) {
|
|
|
193
193
|
}
|
|
194
194
|
return true
|
|
195
195
|
}
|
|
196
|
-
|
|
197
196
|
// prop & value 校验:过滤的不合法的属性和属性值
|
|
198
197
|
const verification = ({ prop, value, selector }, { mode }) => {
|
|
199
198
|
return verifyProps({ prop, value, selector }, { mode }) && verifyValues({ prop, value, selector }) && ({ prop, value })
|
|
@@ -426,7 +425,6 @@ module.exports = function getSpec({ warn, error }) {
|
|
|
426
425
|
values.sort()
|
|
427
426
|
const transform = []
|
|
428
427
|
values.forEach(item => {
|
|
429
|
-
// const match = item.match(/(\w+)\(([^()]+|\s*var\([^)]+\))\)/)
|
|
430
428
|
const match = item.match(/([/\w]+)\((.+)\)/)
|
|
431
429
|
if (match && match.length >= 3) {
|
|
432
430
|
let key = match[1]
|
|
@@ -18,6 +18,18 @@ module.exports = function ({ print }) {
|
|
|
18
18
|
|
|
19
19
|
return {
|
|
20
20
|
test: TAG_NAME,
|
|
21
|
+
ios (tag, { el }) {
|
|
22
|
+
el.isBuiltIn = true
|
|
23
|
+
return 'mpx-camera'
|
|
24
|
+
},
|
|
25
|
+
android (tag, { el }) {
|
|
26
|
+
el.isBuiltIn = true
|
|
27
|
+
return 'mpx-camera'
|
|
28
|
+
},
|
|
29
|
+
harmony (tag, { el }) {
|
|
30
|
+
el.isBuiltIn = true
|
|
31
|
+
return 'mpx-camera'
|
|
32
|
+
},
|
|
21
33
|
props: [
|
|
22
34
|
{
|
|
23
35
|
test: 'mode',
|
|
@@ -13,7 +13,7 @@ const JD_UNSUPPORTED_TAG_NAME_ARR = ['functional-page-navigator', 'live-pusher',
|
|
|
13
13
|
// 快应用不支持的标签集合
|
|
14
14
|
const QA_UNSUPPORTED_TAG_NAME_ARR = ['movable-view', 'movable-area', 'open-data', 'official-account', 'editor', 'functional-page-navigator', 'live-player', 'live-pusher', 'ad', 'cover-image']
|
|
15
15
|
// RN不支持的标签集合
|
|
16
|
-
const RN_UNSUPPORTED_TAG_NAME_ARR = ['open-data', 'official-account', 'editor', 'functional-page-navigator', 'live-player', 'live-pusher', 'ad', 'audio', '
|
|
16
|
+
const RN_UNSUPPORTED_TAG_NAME_ARR = ['open-data', 'official-account', 'editor', 'functional-page-navigator', 'live-player', 'live-pusher', 'ad', 'audio', 'match-media', 'page-container', 'editor', 'keyboard-accessory', 'map']
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* @param {function(object): function} print
|
|
@@ -24,7 +24,7 @@ module.exports = function (styles, {
|
|
|
24
24
|
new Error('[Mpx style error][' + loaderContext.resource + ']: ' + msg)
|
|
25
25
|
)
|
|
26
26
|
}
|
|
27
|
-
const { mode, srcMode
|
|
27
|
+
const { mode, srcMode } = loaderContext.getMpx()
|
|
28
28
|
async.eachOfSeries(styles, (style, i, callback) => {
|
|
29
29
|
const scoped = style.scoped || autoScope
|
|
30
30
|
const extraOptions = {
|
|
@@ -70,44 +70,13 @@ module.exports = function (styles, {
|
|
|
70
70
|
}, '')
|
|
71
71
|
if (ctorType === 'app') {
|
|
72
72
|
output += `
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
global.__getUnoStyle = function(className) {
|
|
81
|
-
if (!__unoClassMap) {
|
|
82
|
-
__unoClassMap = {__unoCssMapPlaceholder__}
|
|
83
|
-
}
|
|
84
|
-
return global.__GCC(className, __unoClassMap, __classCache);
|
|
85
|
-
};
|
|
86
|
-
var __unoVarClassMap;
|
|
87
|
-
global.__getUnoVarStyle = function(className) {
|
|
88
|
-
if (!__unoVarClassMap) {
|
|
89
|
-
__unoVarClassMap = {__unoVarUtilitiesCssMap__}
|
|
90
|
-
}
|
|
91
|
-
return global.__GCC(className, __unoVarClassMap, __classCache);
|
|
92
|
-
};\n`
|
|
93
|
-
output += `
|
|
94
|
-
var __appClassMap
|
|
95
|
-
global.__getAppClassStyle = function(className) {
|
|
96
|
-
if(!__appClassMap) {
|
|
97
|
-
__appClassMap = {__unoCssMapPreflights__, ${classMapCode}};
|
|
98
|
-
}
|
|
99
|
-
return global.__GCC(className, __appClassMap, __classCache);
|
|
100
|
-
};\n`
|
|
101
|
-
} else {
|
|
102
|
-
output += `
|
|
103
|
-
var __appClassMap
|
|
104
|
-
global.__getAppClassStyle = function(className) {
|
|
105
|
-
if(!__appClassMap) {
|
|
106
|
-
__appClassMap = {${classMapCode}};
|
|
107
|
-
}
|
|
108
|
-
return global.__GCC(className, __appClassMap, __classCache);
|
|
109
|
-
};\n`
|
|
110
|
-
}
|
|
73
|
+
var __appClassMap
|
|
74
|
+
global.__getAppClassStyle = function(className) {
|
|
75
|
+
if(!__appClassMap) {
|
|
76
|
+
__appClassMap = {${classMapCode}};
|
|
77
|
+
}
|
|
78
|
+
return global.__GCC(className, __appClassMap, __classCache);
|
|
79
|
+
};\n`
|
|
111
80
|
} else {
|
|
112
81
|
output += `
|
|
113
82
|
var __classMap
|
|
@@ -87,12 +87,6 @@ function getClassMap ({ content, filename, mode, srcMode, ctorType, formatValueN
|
|
|
87
87
|
|
|
88
88
|
root.walkRules(rule => {
|
|
89
89
|
const classMapValue = {}
|
|
90
|
-
const prev = rule.prev()
|
|
91
|
-
let layer
|
|
92
|
-
if (prev && prev.type === 'comment' && prev.text.includes('rn-layer:')) {
|
|
93
|
-
layer = JSON.stringify(prev.text.split(':')[1].trim())
|
|
94
|
-
}
|
|
95
|
-
|
|
96
90
|
rule.walkDecls(({ prop, value }) => {
|
|
97
91
|
if (value === 'undefined' || cssPrefixExp.test(prop) || cssPrefixExp.test(value)) return
|
|
98
92
|
let newData = rulesRunner({ prop, value, selector: rule.selector })
|
|
@@ -124,6 +118,7 @@ function getClassMap ({ content, filename, mode, srcMode, ctorType, formatValueN
|
|
|
124
118
|
classMapValue[prop] = value
|
|
125
119
|
})
|
|
126
120
|
})
|
|
121
|
+
|
|
127
122
|
const classMapKeys = []
|
|
128
123
|
const options = getMediaOptions(rule.parent.params || '')
|
|
129
124
|
const isMedia = options.maxWidth || options.minWidth
|
|
@@ -131,20 +126,15 @@ function getClassMap ({ content, filename, mode, srcMode, ctorType, formatValueN
|
|
|
131
126
|
selectors.each(selector => {
|
|
132
127
|
if (selector.nodes.length === 1 && selector.nodes[0].type === 'class') {
|
|
133
128
|
classMapKeys.push(selector.nodes[0].value)
|
|
134
|
-
} else if (selector.nodes.length === 2 && selector.nodes[0].type === 'class' && selector.nodes[1].type === 'pseudo') {
|
|
135
|
-
classMapKeys.push(selector.nodes[0].value + selector.nodes[1].value)
|
|
136
129
|
} else {
|
|
137
130
|
error('Only single class selector is supported in react native mode temporarily.')
|
|
138
131
|
}
|
|
139
132
|
})
|
|
140
133
|
}).processSync(rule.selector)
|
|
134
|
+
|
|
141
135
|
if (classMapKeys.length) {
|
|
142
136
|
classMapKeys.forEach((key) => {
|
|
143
137
|
if (Object.keys(classMapValue).length) {
|
|
144
|
-
const layerObj = layer ? { _layer: layer } : {}
|
|
145
|
-
if (key.endsWith('!')) {
|
|
146
|
-
layerObj._layer = '"important"'
|
|
147
|
-
}
|
|
148
138
|
let _default = classMap[key]?._default
|
|
149
139
|
let _media = classMap[key]?._media
|
|
150
140
|
if (isMedia) {
|
|
@@ -157,16 +147,15 @@ function getClassMap ({ content, filename, mode, srcMode, ctorType, formatValueN
|
|
|
157
147
|
})
|
|
158
148
|
classMap[key] = {
|
|
159
149
|
_media,
|
|
160
|
-
_default
|
|
161
|
-
...layerObj
|
|
150
|
+
_default
|
|
162
151
|
}
|
|
163
152
|
} else if (_default) {
|
|
164
153
|
// 已有媒体查询数据,此次非媒体查询
|
|
165
|
-
Object.assign(_default, classMapValue
|
|
154
|
+
Object.assign(_default, classMapValue)
|
|
166
155
|
} else {
|
|
167
156
|
// 无媒体查询
|
|
168
157
|
const val = classMap[key] || {}
|
|
169
|
-
classMap[key] = Object.assign(val, classMapValue
|
|
158
|
+
classMap[key] = Object.assign(val, classMapValue)
|
|
170
159
|
}
|
|
171
160
|
}
|
|
172
161
|
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const { EXTEND_COMPONENT_CONFIG } = require('../utils/const')
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 扩展组件路径解析插件
|
|
5
|
+
* 将 @mpxjs/webpack-plugin/lib/runtime/components/extends/[component-name] 格式的路径
|
|
6
|
+
* 解析为对应平台的实际组件路径
|
|
7
|
+
*/
|
|
8
|
+
module.exports = class ExtendComponentsPlugin {
|
|
9
|
+
constructor (source, mode, target) {
|
|
10
|
+
this.source = source
|
|
11
|
+
this.target = target
|
|
12
|
+
this.mode = mode
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
apply (resolver) {
|
|
16
|
+
const target = resolver.ensureHook(this.target)
|
|
17
|
+
const mode = this.mode
|
|
18
|
+
|
|
19
|
+
resolver.getHook(this.source).tapAsync('ExtendComponentsPlugin', (request, resolveContext, callback) => {
|
|
20
|
+
const requestPath = request.request
|
|
21
|
+
if (!requestPath || !requestPath.startsWith('@mpxjs/webpack-plugin/lib/runtime/components/extends/')) {
|
|
22
|
+
return callback()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 匹配 @mpxjs/webpack-plugin/lib/runtime/components/extends/[component-name]
|
|
26
|
+
const extendsMatch = requestPath.match(/^@mpxjs\/webpack-plugin\/lib\/runtime\/components\/extends\/(.+)$/)
|
|
27
|
+
|
|
28
|
+
if (!extendsMatch) {
|
|
29
|
+
return callback()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const componentName = extendsMatch[1]
|
|
33
|
+
|
|
34
|
+
// 检查组件是否在配置中
|
|
35
|
+
if (!EXTEND_COMPONENT_CONFIG[componentName]) {
|
|
36
|
+
return callback(new Error(`Extended component "${componentName}" was not found. Available extended components: ${Object.keys(EXTEND_COMPONENT_CONFIG).join(', ')}`))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 获取当前模式下的组件路径
|
|
40
|
+
const componentConfig = EXTEND_COMPONENT_CONFIG[componentName]
|
|
41
|
+
const newRequest = componentConfig[mode]
|
|
42
|
+
|
|
43
|
+
if (!newRequest) {
|
|
44
|
+
return callback(new Error(`Extended component "${componentName}" cannot be used on the ${mode} platform. Supported platforms include: ${Object.keys(componentConfig).join(', ')}`))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const obj = Object.assign({}, request, {
|
|
48
|
+
request: newRequest
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
resolver.doResolve(
|
|
52
|
+
target,
|
|
53
|
+
obj,
|
|
54
|
+
`resolve extend component: ${componentName} to ${newRequest}`,
|
|
55
|
+
resolveContext,
|
|
56
|
+
callback
|
|
57
|
+
)
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
}
|