markdown-magic 2.6.0 → 3.0.0
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/README.md +6 -10
- package/cli.js +5 -82
- package/lib/block-parser-js.test.js +179 -0
- package/lib/block-parser.js +389 -0
- package/lib/{utils/new-parser.test.js → block-parser.test.js} +168 -50
- package/lib/cli.js +234 -0
- package/lib/cli.test.js +409 -0
- package/lib/defaults.js +12 -0
- package/lib/index.js +319 -184
- package/lib/index.test.js +11 -0
- package/lib/options-parser.js +498 -0
- package/lib/options-parser.test.js +1237 -0
- package/lib/process-contents.js +330 -0
- package/lib/process-file.js +34 -0
- package/lib/transforms/code.js +67 -22
- package/lib/transforms/file.js +13 -10
- package/lib/transforms/remote.js +9 -6
- package/lib/transforms/toc.js +136 -64
- package/lib/transforms/wordCount.js +5 -0
- package/lib/utils/fs.js +340 -0
- package/lib/utils/fs.test.js +268 -0
- package/lib/utils/html-to-json/compat.js +42 -0
- package/lib/utils/html-to-json/format.js +64 -0
- package/lib/utils/html-to-json/index.js +37 -0
- package/lib/utils/html-to-json/lexer.js +345 -0
- package/lib/utils/html-to-json/parser.js +146 -0
- package/lib/utils/html-to-json/stringify.js +37 -0
- package/lib/utils/html-to-json/tags.js +171 -0
- package/lib/utils/index.js +19 -0
- package/{cli-utils.js → lib/utils/load-config.js} +2 -6
- package/lib/utils/logs.js +89 -0
- package/lib/utils/md/filters.js +20 -0
- package/lib/utils/md/find-code-blocks.js +80 -0
- package/lib/utils/md/find-date.js +32 -0
- package/lib/utils/md/find-frontmatter.js +94 -0
- package/lib/utils/md/find-frontmatter.test.js +17 -0
- package/lib/utils/md/find-html-tags.js +105 -0
- package/lib/utils/md/find-images.js +102 -0
- package/lib/utils/md/find-links.js +202 -0
- package/lib/utils/md/find-unmatched-html-tags.js +33 -0
- package/lib/utils/md/fixtures/2022-01-22-date-in-filename.md +14 -0
- package/lib/utils/md/fixtures/file-with-frontmatter.md +32 -0
- package/lib/utils/md/fixtures/file-with-links.md +143 -0
- package/lib/utils/md/md.test.js +37 -0
- package/lib/utils/md/parse.js +122 -0
- package/lib/utils/md/utils.js +19 -0
- package/lib/utils/regex-timeout.js +83 -0
- package/lib/utils/regex.js +38 -5
- package/lib/utils/remoteRequest.js +54 -0
- package/lib/utils/syntax.js +79 -0
- package/lib/utils/text.js +260 -0
- package/lib/utils/text.test.js +311 -0
- package/package.json +26 -26
- package/index.js +0 -46
- package/lib/processFile.js +0 -154
- package/lib/transforms/index.js +0 -114
- package/lib/updateContents.js +0 -125
- package/lib/utils/_md.test.js +0 -63
- package/lib/utils/new-parser.js +0 -412
- package/lib/utils/weird-parse.js +0 -230
- package/lib/utils/weird-parse.test.js +0 -217
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const { test } = require('uvu')
|
|
3
|
+
const assert = require('uvu/assert')
|
|
4
|
+
const { glob, globWithGit } = require('smart-glob')
|
|
5
|
+
const GREEN = '\x1b[32m%s\x1b[0m';
|
|
6
|
+
const { findUp, getFilePaths, getGitignoreContents, convertToRelativePath } = require('./fs')
|
|
7
|
+
|
|
8
|
+
const ROOT_DIR = path.resolve(__dirname, '../../')
|
|
9
|
+
|
|
10
|
+
test('Exports API', () => {
|
|
11
|
+
assert.equal(typeof findUp, 'function', 'undefined val')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('Finds file from file', async () => {
|
|
15
|
+
const startDir = path.resolve(__dirname, '../index.js')
|
|
16
|
+
const file = await findUp(startDir, 'README.md')
|
|
17
|
+
assert.ok(file)
|
|
18
|
+
assert.equal(path.basename(file), 'README.md')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('Finds file from dir', async () => {
|
|
22
|
+
const startDir = path.resolve(__dirname, '../')
|
|
23
|
+
const file = await findUp(startDir, 'README.md')
|
|
24
|
+
assert.ok(file)
|
|
25
|
+
assert.equal(path.basename(file), 'README.md')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('getFilePaths /\.test\.js?$/', async () => {
|
|
29
|
+
const files = await getFilePaths(ROOT_DIR, {
|
|
30
|
+
patterns: [
|
|
31
|
+
/\.test\.js?$/,
|
|
32
|
+
],
|
|
33
|
+
ignore: [
|
|
34
|
+
/node_modules/,
|
|
35
|
+
],
|
|
36
|
+
})
|
|
37
|
+
const foundFiles = convertToRelative(files, ROOT_DIR)
|
|
38
|
+
console.log('foundFiles', foundFiles)
|
|
39
|
+
assert.is(Array.isArray(files), true)
|
|
40
|
+
assert.equal(foundFiles, [
|
|
41
|
+
'lib/block-parser-js.test.js',
|
|
42
|
+
'lib/block-parser.test.js',
|
|
43
|
+
'lib/cli.test.js',
|
|
44
|
+
'lib/index.test.js',
|
|
45
|
+
'lib/options-parser.test.js',
|
|
46
|
+
'lib/utils/fs.test.js',
|
|
47
|
+
"lib/utils/md/find-frontmatter.test.js",
|
|
48
|
+
"lib/utils/md/md.test.js",
|
|
49
|
+
"lib/utils/text.test.js",
|
|
50
|
+
'test/errors.test.js',
|
|
51
|
+
'test/transforms.test.js'
|
|
52
|
+
])
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('getFilePaths /\.mdx?$/, /\.test\.js?$/', async () => {
|
|
56
|
+
const files = await getFilePaths(ROOT_DIR, {
|
|
57
|
+
patterns: [
|
|
58
|
+
/fixtures\/md\/(.*)\.mdx?$/,
|
|
59
|
+
// /\.js$/,
|
|
60
|
+
/\.test\.js?$/,
|
|
61
|
+
],
|
|
62
|
+
ignore: [
|
|
63
|
+
// /^node_modules\//,
|
|
64
|
+
/node_modules/,
|
|
65
|
+
// /\.git/,
|
|
66
|
+
// /NOTES\.md/
|
|
67
|
+
],
|
|
68
|
+
//excludeGitIgnore: true,
|
|
69
|
+
excludeHidden: true,
|
|
70
|
+
})
|
|
71
|
+
const foundFiles = convertToRelative(files, ROOT_DIR)
|
|
72
|
+
//console.log('foundFiles', foundFiles)
|
|
73
|
+
assert.is(Array.isArray(files), true)
|
|
74
|
+
assert.equal(foundFiles, [
|
|
75
|
+
'lib/block-parser-js.test.js',
|
|
76
|
+
'lib/block-parser.test.js',
|
|
77
|
+
'lib/cli.test.js',
|
|
78
|
+
'lib/index.test.js',
|
|
79
|
+
'lib/options-parser.test.js',
|
|
80
|
+
'lib/utils/fs.test.js',
|
|
81
|
+
"lib/utils/md/find-frontmatter.test.js",
|
|
82
|
+
"lib/utils/md/md.test.js",
|
|
83
|
+
"lib/utils/text.test.js",
|
|
84
|
+
'test/errors.test.js',
|
|
85
|
+
'test/fixtures/md/basic.md',
|
|
86
|
+
'test/fixtures/md/error-missing-transforms-two.md',
|
|
87
|
+
'test/fixtures/md/error-missing-transforms.md',
|
|
88
|
+
'test/fixtures/md/error-no-block-transform-defined.md',
|
|
89
|
+
'test/fixtures/md/error-unbalanced.md',
|
|
90
|
+
'test/fixtures/md/format-inline.md',
|
|
91
|
+
'test/fixtures/md/format-with-wacky-indentation.md',
|
|
92
|
+
'test/fixtures/md/inline-two.md',
|
|
93
|
+
'test/fixtures/md/inline.md',
|
|
94
|
+
'test/fixtures/md/mdx-file.mdx',
|
|
95
|
+
'test/fixtures/md/missing-transform.md',
|
|
96
|
+
'test/fixtures/md/mixed.md',
|
|
97
|
+
'test/fixtures/md/nested/nested.md',
|
|
98
|
+
'test/fixtures/md/no-transforms.md',
|
|
99
|
+
'test/fixtures/md/string.md',
|
|
100
|
+
'test/fixtures/md/syntax-legacy-colon.md',
|
|
101
|
+
'test/fixtures/md/syntax-legacy-query.md',
|
|
102
|
+
'test/fixtures/md/syntax-mixed.md',
|
|
103
|
+
'test/fixtures/md/transform-code.md',
|
|
104
|
+
'test/fixtures/md/transform-custom.md',
|
|
105
|
+
'test/fixtures/md/transform-file.md',
|
|
106
|
+
'test/fixtures/md/transform-remote.md',
|
|
107
|
+
'test/fixtures/md/transform-toc.md',
|
|
108
|
+
'test/fixtures/md/transform-wordCount.md',
|
|
109
|
+
'test/transforms.test.js'
|
|
110
|
+
])
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('getFilePaths glob', async () => {
|
|
114
|
+
/*
|
|
115
|
+
const x = await glob('**.md')
|
|
116
|
+
console.log(x)
|
|
117
|
+
process.exit(1)
|
|
118
|
+
/** */
|
|
119
|
+
|
|
120
|
+
const files = await getFilePaths(ROOT_DIR, {
|
|
121
|
+
patterns: [
|
|
122
|
+
/\.test\.js?$/,
|
|
123
|
+
// /(.*)\.mdx?$/,
|
|
124
|
+
'test/fixtures/md/**.{md,mdx}'
|
|
125
|
+
// /^[^\/]+\.md?$/,
|
|
126
|
+
// '**.json',
|
|
127
|
+
// '**/**.js',
|
|
128
|
+
// '**.md',
|
|
129
|
+
//'/(.*).md$/',
|
|
130
|
+
// '/^test/',
|
|
131
|
+
// 'test/**'
|
|
132
|
+
///(.*)\.md/g
|
|
133
|
+
],
|
|
134
|
+
ignore: [
|
|
135
|
+
// /^node_modules\//,
|
|
136
|
+
/node_modules/,
|
|
137
|
+
// /(.*)\.js$/,
|
|
138
|
+
// /\.git/,
|
|
139
|
+
// /NOTES\.md/
|
|
140
|
+
],
|
|
141
|
+
excludeGitIgnore: true,
|
|
142
|
+
excludeHidden: true,
|
|
143
|
+
})
|
|
144
|
+
const foundFiles = convertToRelative(files, ROOT_DIR)
|
|
145
|
+
console.log('foundFiles', foundFiles)
|
|
146
|
+
/*
|
|
147
|
+
aggregateReports()
|
|
148
|
+
process.exit(1)
|
|
149
|
+
/** */
|
|
150
|
+
assert.is(Array.isArray(files), true)
|
|
151
|
+
assert.equal(foundFiles, [
|
|
152
|
+
'lib/block-parser-js.test.js',
|
|
153
|
+
'lib/block-parser.test.js',
|
|
154
|
+
'lib/cli.test.js',
|
|
155
|
+
'lib/index.test.js',
|
|
156
|
+
'lib/options-parser.test.js',
|
|
157
|
+
'lib/utils/fs.test.js',
|
|
158
|
+
"lib/utils/md/find-frontmatter.test.js",
|
|
159
|
+
"lib/utils/md/md.test.js",
|
|
160
|
+
'lib/utils/text.test.js',
|
|
161
|
+
// 'misc/old-test/main.test.js',
|
|
162
|
+
'test/errors.test.js',
|
|
163
|
+
'test/fixtures/md/basic.md',
|
|
164
|
+
'test/fixtures/md/error-missing-transforms-two.md',
|
|
165
|
+
'test/fixtures/md/error-missing-transforms.md',
|
|
166
|
+
'test/fixtures/md/error-no-block-transform-defined.md',
|
|
167
|
+
'test/fixtures/md/error-unbalanced.md',
|
|
168
|
+
'test/fixtures/md/format-inline.md',
|
|
169
|
+
'test/fixtures/md/format-with-wacky-indentation.md',
|
|
170
|
+
'test/fixtures/md/inline-two.md',
|
|
171
|
+
'test/fixtures/md/inline.md',
|
|
172
|
+
'test/fixtures/md/mdx-file.mdx',
|
|
173
|
+
'test/fixtures/md/missing-transform.md',
|
|
174
|
+
'test/fixtures/md/mixed.md',
|
|
175
|
+
'test/fixtures/md/no-transforms.md',
|
|
176
|
+
'test/fixtures/md/string.md',
|
|
177
|
+
'test/fixtures/md/syntax-legacy-colon.md',
|
|
178
|
+
'test/fixtures/md/syntax-legacy-query.md',
|
|
179
|
+
'test/fixtures/md/syntax-mixed.md',
|
|
180
|
+
'test/fixtures/md/transform-code.md',
|
|
181
|
+
'test/fixtures/md/transform-custom.md',
|
|
182
|
+
'test/fixtures/md/transform-file.md',
|
|
183
|
+
'test/fixtures/md/transform-remote.md',
|
|
184
|
+
'test/fixtures/md/transform-toc.md',
|
|
185
|
+
'test/fixtures/md/transform-wordCount.md',
|
|
186
|
+
'test/transforms.test.js'
|
|
187
|
+
])
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test('getGitignoreContents', async () => {
|
|
191
|
+
const files = await getGitignoreContents()
|
|
192
|
+
console.log('files', files)
|
|
193
|
+
assert.is(Array.isArray(files), true)
|
|
194
|
+
assert.equal(files, [
|
|
195
|
+
'logs',
|
|
196
|
+
'*.log',
|
|
197
|
+
'npm-debug.log*',
|
|
198
|
+
'yarn-debug.log*',
|
|
199
|
+
'yarn-error.log*',
|
|
200
|
+
'test/fixtures/output',
|
|
201
|
+
'_out.md',
|
|
202
|
+
'misc',
|
|
203
|
+
'misc/**/**.js',
|
|
204
|
+
'**/old-test/cool.md',
|
|
205
|
+
'pids',
|
|
206
|
+
'*.pid',
|
|
207
|
+
'*.seed',
|
|
208
|
+
'*.pid.lock',
|
|
209
|
+
'lib-cov',
|
|
210
|
+
'coverage',
|
|
211
|
+
'.nyc_output',
|
|
212
|
+
'.grunt',
|
|
213
|
+
'bower_components',
|
|
214
|
+
'.lock-wscript',
|
|
215
|
+
'build/Release',
|
|
216
|
+
'node_modules',
|
|
217
|
+
'jspm_packages',
|
|
218
|
+
'typings',
|
|
219
|
+
'.npm',
|
|
220
|
+
'.eslintcache',
|
|
221
|
+
'.node_repl_history',
|
|
222
|
+
'*.tgz',
|
|
223
|
+
'.yarn-integrity',
|
|
224
|
+
'.env',
|
|
225
|
+
'.env.test',
|
|
226
|
+
'.cache',
|
|
227
|
+
'.next',
|
|
228
|
+
'.nuxt',
|
|
229
|
+
'.vuepress/dist',
|
|
230
|
+
'.serverless',
|
|
231
|
+
'.fusebox',
|
|
232
|
+
'.dynamodb',
|
|
233
|
+
'.DS_Store',
|
|
234
|
+
'.AppleDouble',
|
|
235
|
+
'.LSOverride',
|
|
236
|
+
'Icon',
|
|
237
|
+
'._*',
|
|
238
|
+
'.DocumentRevisions-V100',
|
|
239
|
+
'.fseventsd',
|
|
240
|
+
'.Spotlight-V100',
|
|
241
|
+
'.TemporaryItems',
|
|
242
|
+
'.Trashes',
|
|
243
|
+
'.VolumeIcon.icns',
|
|
244
|
+
'.com.apple.timemachine.donotpresent',
|
|
245
|
+
'.AppleDB',
|
|
246
|
+
'.AppleDesktop',
|
|
247
|
+
'Network Trash Folder',
|
|
248
|
+
'Temporary Items',
|
|
249
|
+
'.apdisk'
|
|
250
|
+
])
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
async function aggregateReports() {
|
|
254
|
+
console.log(GREEN, `Done.`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function convertToRelative(files, dir) {
|
|
258
|
+
return files.map((f) => convertToRelativePath(f, dir)).sort()
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function getIgnores(dir){
|
|
262
|
+
const files = await getGitignoreContents()
|
|
263
|
+
console.log('files', files)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
//getIgnores(process.cwd())
|
|
267
|
+
|
|
268
|
+
test.run()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
|
|
2
|
+
function startsWith (str, searchString, position) {
|
|
3
|
+
return str.substr(position || 0, searchString.length) === searchString
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function endsWith (str, searchString, position) {
|
|
7
|
+
const index = (position || str.length) - searchString.length
|
|
8
|
+
const lastIndex = str.lastIndexOf(searchString, index)
|
|
9
|
+
return lastIndex !== -1 && lastIndex === index
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function stringIncludes (str, searchString, position) {
|
|
13
|
+
return str.indexOf(searchString, position || 0) !== -1
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isRealNaN (x) {
|
|
17
|
+
return typeof x === 'number' && isNaN(x)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function arrayIncludes (array, searchElement, position) {
|
|
21
|
+
const len = array.length
|
|
22
|
+
if (len === 0) return false
|
|
23
|
+
|
|
24
|
+
const lookupIndex = position | 0
|
|
25
|
+
const isNaNElement = isRealNaN(searchElement)
|
|
26
|
+
let searchIndex = lookupIndex >= 0 ? lookupIndex : len + lookupIndex
|
|
27
|
+
while (searchIndex < len) {
|
|
28
|
+
const element = array[searchIndex++]
|
|
29
|
+
if (element === searchElement) return true
|
|
30
|
+
if (isNaNElement && isRealNaN(element)) return true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = {
|
|
37
|
+
startsWith,
|
|
38
|
+
endsWith,
|
|
39
|
+
stringIncludes,
|
|
40
|
+
isRealNaN,
|
|
41
|
+
arrayIncludes
|
|
42
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
|
|
2
|
+
function splitHead(str, sep) {
|
|
3
|
+
const idx = str.indexOf(sep)
|
|
4
|
+
if (idx === -1) return [str]
|
|
5
|
+
return [str.slice(0, idx), str.slice(idx + sep.length)]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function unquote(str) {
|
|
9
|
+
const car = str.charAt(0)
|
|
10
|
+
const end = str.length - 1
|
|
11
|
+
const isQuoteStart = car === '"' || car === "'"
|
|
12
|
+
if (isQuoteStart && car === str.charAt(end)) {
|
|
13
|
+
return str.slice(1, end)
|
|
14
|
+
}
|
|
15
|
+
return str
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function format(nodes, options) {
|
|
19
|
+
return nodes.map(node => {
|
|
20
|
+
const type = node.type
|
|
21
|
+
let outputNode = { type, content: node.content }
|
|
22
|
+
if (type === 'element') {
|
|
23
|
+
outputNode = {
|
|
24
|
+
// TODO maybe harden with https://github.com/riot/dom-nodes
|
|
25
|
+
type: (/^[A-Z]/.test(node.tagName)) ? 'component' : type,
|
|
26
|
+
// isReactComponent: /^[A-Z]/.test(node.tagName),
|
|
27
|
+
tagName: node.tagName,
|
|
28
|
+
props: node.props,
|
|
29
|
+
propsRaw: node.propsRaw,
|
|
30
|
+
children: format(node.children, options)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (options.includePositions) {
|
|
34
|
+
if (options.offset) {
|
|
35
|
+
const { lineOffset, charOffset } = options.offset
|
|
36
|
+
node.position.start.line = node.position.start.line + lineOffset
|
|
37
|
+
node.position.start.index = node.position.start.index + charOffset
|
|
38
|
+
node.position.end.line = node.position.end.line + lineOffset
|
|
39
|
+
node.position.end.index = node.position.end.index + charOffset
|
|
40
|
+
}
|
|
41
|
+
outputNode.position = node.position
|
|
42
|
+
}
|
|
43
|
+
return outputNode
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Old
|
|
48
|
+
function formatAttributes (attributes) {
|
|
49
|
+
return attributes.map(attribute => {
|
|
50
|
+
const parts = splitHead(attribute.trim(), '=')
|
|
51
|
+
const key = parts[0]
|
|
52
|
+
const value = typeof parts[1] === 'string'
|
|
53
|
+
? unquote(parts[1])
|
|
54
|
+
: null
|
|
55
|
+
return {key, value}
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
splitHead,
|
|
61
|
+
unquote,
|
|
62
|
+
format,
|
|
63
|
+
formatAttributes
|
|
64
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Fork of https://github.com/andrejewski/himalaya with tweaks
|
|
2
|
+
const { lexer } = require('./lexer')
|
|
3
|
+
const { parser } = require('./parser')
|
|
4
|
+
const { format } = require('./format')
|
|
5
|
+
const { toHTML } = require('./stringify')
|
|
6
|
+
const {
|
|
7
|
+
voidTags,
|
|
8
|
+
closingTags,
|
|
9
|
+
childlessTags,
|
|
10
|
+
closingTagAncestorBreakers
|
|
11
|
+
} = require('./tags')
|
|
12
|
+
|
|
13
|
+
const parseDefaults = {
|
|
14
|
+
voidTags,
|
|
15
|
+
closingTags,
|
|
16
|
+
childlessTags,
|
|
17
|
+
closingTagAncestorBreakers,
|
|
18
|
+
includePositions: false
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parse(str, opts = {}) {
|
|
22
|
+
const options = Object.assign(parseDefaults, opts)
|
|
23
|
+
const tokens = lexer(str, options)
|
|
24
|
+
const nodes = parser(tokens, options)
|
|
25
|
+
return format(nodes, options)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function stringify(ast, opts = {}) {
|
|
29
|
+
const options = Object.assign(parseDefaults, opts)
|
|
30
|
+
return toHTML(ast, options)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
parseDefaults,
|
|
35
|
+
parse,
|
|
36
|
+
stringify
|
|
37
|
+
}
|