@gesslar/bedoc 1.0.2 → 1.2.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 +4 -4
- package/package.json +1 -2
- package/src/cli.js +12 -11
- package/src/core/ActionManager.js +85 -22
- package/src/core/Configuration.js +6 -0
- package/src/core/Conveyor.js +20 -11
- package/src/core/Core.js +68 -78
- package/src/core/Discovery.js +219 -65
- package/src/core/{HooksManager.js → HookManager.js} +14 -13
- package/src/core/Logger.js +2 -2
- package/src/core/action/ParseManager.js +0 -19
- package/src/core/action/PrintManager.js +0 -19
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
# BeDoc
|
|
2
|
-
|
|
3
1
|
[](https://github.com/gesslar/BeDoc/actions/workflows/codeql.yml)
|
|
4
2
|
[](https://github.com/gesslar/BeDoc/actions/workflows/dependabot/dependabot-updates)
|
|
5
3
|
[](https://github.com/gesslar/BeDoc/actions/workflows/autopr-dev.yml)
|
|
6
4
|
|
|
7
|
-
|
|
8
5
|
# BeDoc
|
|
9
6
|
|
|
10
|
-
**BeDoc** is a powerful, pluggable documentation generator designed to handle
|
|
7
|
+
**BeDoc** is a powerful, pluggable documentation generator designed to handle
|
|
8
|
+
any programming language and output format. With its extensible framework, you
|
|
9
|
+
can easily create custom parsers and printers to generate structured
|
|
10
|
+
documentation for your projects.
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gesslar/bedoc",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Pluggable documentation engine for any language and format",
|
|
5
5
|
"publisher": "gesslar",
|
|
6
6
|
"main": "./src/core/Core.js",
|
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
"globby": "^14.0.2",
|
|
28
28
|
"micromatch": "^4.0.8",
|
|
29
29
|
"node-fetch": "^3.3.2",
|
|
30
|
-
"vscode-uri": "^3.0.8",
|
|
31
30
|
"yaml": "^2.7.0"
|
|
32
31
|
},
|
|
33
32
|
"devDependencies": {
|
package/src/cli.js
CHANGED
|
@@ -63,7 +63,7 @@ const {resolveDirectory} = FDUtil
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
// Create core instance with validated config
|
|
66
|
-
BeDoc
|
|
66
|
+
const bedoc = await BeDoc
|
|
67
67
|
.new({
|
|
68
68
|
options: {
|
|
69
69
|
...optionsWithSources,
|
|
@@ -72,19 +72,20 @@ const {resolveDirectory} = FDUtil
|
|
|
72
72
|
},
|
|
73
73
|
source: Environment.CLI
|
|
74
74
|
})
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
const filesToProcess = bedoc.options.input.map(f => f.absolutePath)
|
|
76
|
+
const result = await bedoc.processFiles(filesToProcess)
|
|
77
|
+
const errored = result.errored
|
|
78
|
+
if(errored.length > 0)
|
|
79
|
+
throw new AggregateError(errored.map(e => e.error), "Error processing files")
|
|
80
|
+
} catch(error) {
|
|
81
|
+
if(error instanceof Error) {
|
|
82
|
+
if(error instanceof AggregateError) {
|
|
83
|
+
error.errors.forEach(e => console.error(e))
|
|
83
84
|
} else {
|
|
84
|
-
console.error(
|
|
85
|
+
console.error(error.message, error.stack)
|
|
85
86
|
}
|
|
86
87
|
} else {
|
|
87
|
-
console.error(
|
|
88
|
+
console.error("Error: %o", error)
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
process.exit(1)
|
|
@@ -1,27 +1,26 @@
|
|
|
1
|
-
import {hookPoints} from "./
|
|
1
|
+
import {hookPoints} from "./HookManager.js"
|
|
2
2
|
|
|
3
3
|
export default class ActionManager {
|
|
4
4
|
#action = null
|
|
5
|
-
#
|
|
6
|
-
#hooks = null
|
|
5
|
+
#hookManager = null
|
|
7
6
|
#contract
|
|
8
|
-
#module
|
|
9
7
|
#log
|
|
10
8
|
#debug
|
|
9
|
+
#file
|
|
11
10
|
|
|
12
11
|
constructor(actionDefinition, logger) {
|
|
13
12
|
this.#log = logger
|
|
14
13
|
this.#debug = this.#log.newDebug()
|
|
15
14
|
|
|
16
|
-
this.#
|
|
15
|
+
this.#initialize(actionDefinition)
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
#
|
|
18
|
+
#initialize(actionDefinition) {
|
|
20
19
|
const debug = this.#debug
|
|
21
20
|
|
|
22
21
|
debug("Setting up action", 2)
|
|
23
22
|
|
|
24
|
-
const {action,
|
|
23
|
+
const {action, file, contract} = actionDefinition
|
|
25
24
|
|
|
26
25
|
if(!action)
|
|
27
26
|
throw new Error("Action is required")
|
|
@@ -29,13 +28,9 @@ export default class ActionManager {
|
|
|
29
28
|
if(!contract)
|
|
30
29
|
throw new Error("Contract is required")
|
|
31
30
|
|
|
32
|
-
if(!module)
|
|
33
|
-
throw new Error("Module is required")
|
|
34
|
-
|
|
35
|
-
this.#module = module
|
|
36
31
|
this.#action = action
|
|
37
32
|
this.#contract = contract
|
|
38
|
-
this.#
|
|
33
|
+
this.#file = file
|
|
39
34
|
|
|
40
35
|
debug("Action setup complete", 2)
|
|
41
36
|
}
|
|
@@ -44,33 +39,101 @@ export default class ActionManager {
|
|
|
44
39
|
return this.#action
|
|
45
40
|
}
|
|
46
41
|
|
|
47
|
-
get
|
|
48
|
-
return this.#
|
|
42
|
+
get hookManager() {
|
|
43
|
+
return this.#hookManager
|
|
49
44
|
}
|
|
50
45
|
|
|
51
|
-
set
|
|
52
|
-
if(this.
|
|
46
|
+
set hookManager(hookManager) {
|
|
47
|
+
if(this.hookManager)
|
|
53
48
|
throw new Error("Hooks already set")
|
|
54
49
|
|
|
55
50
|
this.action.hook = hookManager.on.bind(this.action)
|
|
56
51
|
this.action.HOOKS = hookPoints
|
|
52
|
+
this.#hookManager = hookManager
|
|
57
53
|
this.action.hooks = hookManager.hooks
|
|
58
|
-
this.#hooks = hookManager
|
|
59
54
|
}
|
|
60
55
|
|
|
61
56
|
get contract() {
|
|
62
57
|
return this.#contract
|
|
63
58
|
}
|
|
64
59
|
|
|
65
|
-
get module() {
|
|
66
|
-
return this.#module
|
|
67
|
-
}
|
|
68
|
-
|
|
69
60
|
get meta() {
|
|
70
|
-
return this.#meta
|
|
61
|
+
return this.#action.meta
|
|
71
62
|
}
|
|
72
63
|
|
|
73
64
|
get log() {
|
|
74
65
|
return this.#log
|
|
75
66
|
}
|
|
67
|
+
|
|
68
|
+
async #setupAction() {
|
|
69
|
+
const setup = this.action?.setup
|
|
70
|
+
|
|
71
|
+
if(!setup)
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
await this.action.setup.call(
|
|
75
|
+
this.action, {parent: this, log: this.#log}
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async #cleanupAction() {
|
|
80
|
+
const cleanup = this.action?.cleanup
|
|
81
|
+
|
|
82
|
+
if(!cleanup)
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
await this.action.cleanup.call(this.action)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async #setupHooks() {
|
|
89
|
+
const setup = this.hookManager?.setup
|
|
90
|
+
|
|
91
|
+
if(!setup)
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
await this.hookManager.setup.call(
|
|
95
|
+
this.hookManager.hooks, {parent: this.action, log: this.#log}
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async #cleanupHooks() {
|
|
100
|
+
const cleanup = this.hookManager?.cleanup
|
|
101
|
+
|
|
102
|
+
if(!cleanup)
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
await this.hookManager.cleanup.call(this.hookManager.hooks)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async setupAction() {
|
|
109
|
+
this.#debug("Setting up action for %s", 2, this.meta.action)
|
|
110
|
+
|
|
111
|
+
await this.#setupHooks()
|
|
112
|
+
await this.#setupAction()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async runAction({file,content}) {
|
|
116
|
+
const func = this.action.run
|
|
117
|
+
|
|
118
|
+
if(!func)
|
|
119
|
+
throw new Error(`No \`run\` function found for action \`${this.meta.action}\``)
|
|
120
|
+
|
|
121
|
+
const actionResult = await func.call(
|
|
122
|
+
this.action, {module: file.module, content}
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return actionResult
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async cleanupAction() {
|
|
129
|
+
this.#debug("Post action", 2)
|
|
130
|
+
this.#debug("Cleaning up action for %s", 2, this.meta.action)
|
|
131
|
+
|
|
132
|
+
await this.#cleanupHooks()
|
|
133
|
+
await this.#cleanupAction()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
toString() {
|
|
137
|
+
return `${this.#file?.module || "UNDEFINED"} (${this.meta?.action || "UNDEFINED"})`
|
|
138
|
+
}
|
|
76
139
|
}
|
|
@@ -159,6 +159,12 @@ export default class Configuration {
|
|
|
159
159
|
options.packageJson = {value: jsonObj, source}
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
// Add defaults which are missing
|
|
163
|
+
for(const [key, param] of Object.entries(ConfigurationParameters)) {
|
|
164
|
+
if(options[key] === undefined && param.default !== undefined)
|
|
165
|
+
options[key] = {value: param.default, source: "default"}
|
|
166
|
+
}
|
|
167
|
+
|
|
162
168
|
return options
|
|
163
169
|
}
|
|
164
170
|
|
package/src/core/Conveyor.js
CHANGED
|
@@ -6,9 +6,9 @@ export default class Conveyor {
|
|
|
6
6
|
#succeeded = []
|
|
7
7
|
#errored = []
|
|
8
8
|
|
|
9
|
-
constructor(
|
|
10
|
-
this.
|
|
11
|
-
this.
|
|
9
|
+
constructor(parse, print, logger, output) {
|
|
10
|
+
this.parse = parse
|
|
11
|
+
this.print = print
|
|
12
12
|
this.logger = logger
|
|
13
13
|
this.output = output
|
|
14
14
|
}
|
|
@@ -23,11 +23,14 @@ export default class Conveyor {
|
|
|
23
23
|
async convey(files, maxConcurrent = 10) {
|
|
24
24
|
const semaphore = Array(maxConcurrent).fill(Promise.resolve())
|
|
25
25
|
|
|
26
|
+
// Set up the actions
|
|
27
|
+
await this.parse.setupAction()
|
|
28
|
+
await this.print.setupAction()
|
|
29
|
+
|
|
26
30
|
for(const file of files) {
|
|
27
31
|
const slot = Promise.race(semaphore) // Wait for an available slot
|
|
28
32
|
semaphore.push(slot.then(async() => {
|
|
29
33
|
const result = await this.#processFile(file)
|
|
30
|
-
|
|
31
34
|
if(result.status === "success")
|
|
32
35
|
this.#succeeded.push({input: file, output: result.file})
|
|
33
36
|
else
|
|
@@ -39,6 +42,10 @@ export default class Conveyor {
|
|
|
39
42
|
// Wait for all tasks to complete
|
|
40
43
|
await Promise.all(semaphore)
|
|
41
44
|
|
|
45
|
+
// Clean up actions
|
|
46
|
+
await this.parse.cleanupAction()
|
|
47
|
+
await this.print.cleanupAction()
|
|
48
|
+
|
|
42
49
|
return {succeeded: this.#succeeded, errored: this.#errored}
|
|
43
50
|
}
|
|
44
51
|
|
|
@@ -50,6 +57,7 @@ export default class Conveyor {
|
|
|
50
57
|
*/
|
|
51
58
|
async #processFile(file) {
|
|
52
59
|
const debug = this.logger.newDebug()
|
|
60
|
+
const {parse, print} = this
|
|
53
61
|
|
|
54
62
|
try {
|
|
55
63
|
debug("Processing file: `%s`", 2, file.path)
|
|
@@ -59,17 +67,20 @@ export default class Conveyor {
|
|
|
59
67
|
debug("Read file content `%s` (%d bytes)", 2, file.path, fileContent.length)
|
|
60
68
|
|
|
61
69
|
// Step 2: Parse file
|
|
62
|
-
const parseResult = await
|
|
70
|
+
const parseResult = await parse.runAction({
|
|
71
|
+
file,
|
|
72
|
+
content: fileContent
|
|
73
|
+
})
|
|
63
74
|
if(parseResult.status === "error")
|
|
64
75
|
return parseResult
|
|
65
76
|
|
|
66
77
|
debug("Parsed file successfully: `%s`", 2, file.path)
|
|
67
78
|
|
|
68
79
|
// Step 3: Print file
|
|
69
|
-
const printResult = await
|
|
80
|
+
const printResult = await print.runAction({
|
|
70
81
|
file,
|
|
71
|
-
parseResult.result,
|
|
72
|
-
)
|
|
82
|
+
content: parseResult.result,
|
|
83
|
+
})
|
|
73
84
|
if(printResult.status === "error")
|
|
74
85
|
return printResult
|
|
75
86
|
|
|
@@ -87,9 +98,7 @@ export default class Conveyor {
|
|
|
87
98
|
|
|
88
99
|
return writeResult
|
|
89
100
|
} catch(error) {
|
|
90
|
-
|
|
91
|
-
this.logger.error(mess)
|
|
92
|
-
return {status: "error", error}
|
|
101
|
+
return {status: "error", file, error}
|
|
93
102
|
}
|
|
94
103
|
}
|
|
95
104
|
|
package/src/core/Core.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import process from "node:process"
|
|
2
2
|
|
|
3
3
|
import Discovery from "./Discovery.js"
|
|
4
|
-
import
|
|
4
|
+
import HookManager from "./HookManager.js"
|
|
5
5
|
import Logger from "./Logger.js"
|
|
6
6
|
import ParseManager from "./action/ParseManager.js"
|
|
7
7
|
import PrintManager from "./action/PrintManager.js"
|
|
@@ -33,100 +33,85 @@ export default class Core {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
static async new({options, source}) {
|
|
36
|
-
const
|
|
36
|
+
const config = new Configuration()
|
|
37
37
|
|
|
38
|
-
const
|
|
39
|
-
if(
|
|
40
|
-
throw new AggregateError(
|
|
38
|
+
const validConfig = await config.validate({options, source})
|
|
39
|
+
if(validConfig.status === "error")
|
|
40
|
+
throw new AggregateError(validConfig.errors,"BeDoc configuration failed")
|
|
41
41
|
|
|
42
|
-
const instance = new Core({...
|
|
42
|
+
const instance = new Core({...validConfig, name: "BeDoc"})
|
|
43
43
|
const debug = instance.logger.newDebug()
|
|
44
44
|
|
|
45
|
-
debug("Creating new BeDoc instance with options: `%o`", 2,
|
|
45
|
+
debug("Creating new BeDoc instance with options: `%o`", 2, validConfig)
|
|
46
46
|
|
|
47
47
|
const discovery = new Discovery(instance)
|
|
48
|
-
const
|
|
48
|
+
const {printer: validPrint, parser: validParse} = validConfig
|
|
49
49
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
50
|
+
const actionDefs = await discovery.discoverActions({
|
|
51
|
+
print: validPrint,
|
|
52
|
+
parse: validParse
|
|
53
|
+
})
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
(a) => a.action.meta[criterion] === validatedConfig[criterion],
|
|
59
|
-
)
|
|
60
|
-
}
|
|
61
|
-
}
|
|
55
|
+
const validCrit = discovery.satisfyCriteria(actionDefs, validConfig)
|
|
56
|
+
|
|
57
|
+
debug("Actions that met criteria: `%o`", 2, validCrit)
|
|
62
58
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
for(const printer of filteredActions.print) {
|
|
66
|
-
for(const parser of filteredActions.parse) {
|
|
67
|
-
const satisfied = schemaCompare(parser.contract, printer.contract)
|
|
59
|
+
if(Object.values(validCrit).some(arr => arr.length === 0))
|
|
60
|
+
throw new Error("No found matching parser and printer")
|
|
68
61
|
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
const validSchemas = {print: [], parse: []}
|
|
63
|
+
let printers = validCrit.print.length
|
|
64
|
+
while(printers--) {
|
|
65
|
+
const printer = validCrit.print[printers]
|
|
66
|
+
const printerSchema = printer.contract
|
|
67
|
+
const satisfied = []
|
|
68
|
+
for(const parser of validCrit.parse) {
|
|
69
|
+
const parserSchema = parser.contract
|
|
70
|
+
const result = schemaCompare(parserSchema, printerSchema)
|
|
71
|
+
if(result.status === "success")
|
|
72
|
+
satisfied.push(parser)
|
|
71
73
|
}
|
|
72
|
-
}
|
|
73
74
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
`${matches.map((m) => m.print.name).join(", ")}`
|
|
79
|
-
throw new Error(message)
|
|
75
|
+
if(satisfied.length > 0) {
|
|
76
|
+
validSchemas.print.push(printer)
|
|
77
|
+
validSchemas.parse.push(...satisfied)
|
|
78
|
+
}
|
|
80
79
|
}
|
|
81
80
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
const finalActions = {}
|
|
82
|
+
for(const [key, value] of Object.entries(validSchemas)) {
|
|
83
|
+
if(value.length === 0)
|
|
84
|
+
throw new Error(`No matching ${key} found`)
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const satisfied = schemaCompare(
|
|
90
|
-
chosenActions.parse.contract,
|
|
91
|
-
chosenActions.print.contract,
|
|
92
|
-
)
|
|
86
|
+
if(value.length > 1)
|
|
87
|
+
throw new Error(`Multiple matching ${key} found`)
|
|
93
88
|
|
|
94
|
-
|
|
95
|
-
instance.logger.error(
|
|
96
|
-
`[Core.new] action contract failed: ${satisfied.errors}`,
|
|
97
|
-
)
|
|
98
|
-
throw new AggregateError(satisfied.errors, "Action contract failed")
|
|
99
|
-
} else if(satisfied.status !== "success") {
|
|
100
|
-
throw new Error(
|
|
101
|
-
`[Core.new] Action contract failed: ${satisfied.message}`,
|
|
102
|
-
)
|
|
89
|
+
finalActions[key] = validSchemas[key][0]
|
|
103
90
|
}
|
|
104
91
|
|
|
105
92
|
debug("Contracts satisfied between parser and printer", 2)
|
|
106
93
|
|
|
107
94
|
// Adding to instance
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
action: action,
|
|
123
|
-
hooksFile: validatedConfig.hooks,
|
|
95
|
+
instance.actions = {}
|
|
96
|
+
const managers = {print: PrintManager, parse: ParseManager}
|
|
97
|
+
for(const [, value] of Object.entries(finalActions)) {
|
|
98
|
+
const {action: actionType} = value.action.meta
|
|
99
|
+
|
|
100
|
+
debug("Attaching `%o` action to instance", 2, actionType)
|
|
101
|
+
instance.actions[actionType] = new managers[actionType](
|
|
102
|
+
value, instance.logger
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if(validConfig.hooks) {
|
|
106
|
+
const hookManager = await HookManager.new({
|
|
107
|
+
action: actionType,
|
|
108
|
+
hooksFile: validConfig.hooks,
|
|
124
109
|
logger: new Logger(instance.debugOptions),
|
|
125
|
-
timeout:
|
|
110
|
+
timeout: validConfig.hooksTimeout,
|
|
126
111
|
})
|
|
127
112
|
|
|
128
|
-
if(
|
|
129
|
-
|
|
113
|
+
if(hookManager)
|
|
114
|
+
instance.actions[actionType].hookManager = hookManager
|
|
130
115
|
}
|
|
131
116
|
}
|
|
132
117
|
|
|
@@ -135,6 +120,7 @@ export default class Core {
|
|
|
135
120
|
|
|
136
121
|
async processFiles(glob, startTime = process.hrtime()) {
|
|
137
122
|
const debug = this.logger.newDebug()
|
|
123
|
+
|
|
138
124
|
debug("Starting file processing with conveyor", 1)
|
|
139
125
|
|
|
140
126
|
const {output} = this.options
|
|
@@ -145,8 +131,8 @@ export default class Core {
|
|
|
145
131
|
|
|
146
132
|
// Instantiate the conveyor
|
|
147
133
|
const conveyor = new Conveyor(
|
|
148
|
-
this.
|
|
149
|
-
this.
|
|
134
|
+
this.actions.parse,
|
|
135
|
+
this.actions.print,
|
|
150
136
|
this.logger,
|
|
151
137
|
output,
|
|
152
138
|
)
|
|
@@ -171,12 +157,16 @@ export default class Core {
|
|
|
171
157
|
|
|
172
158
|
this.logger.debug(message, 1)
|
|
173
159
|
|
|
174
|
-
if(errored.
|
|
175
|
-
const failureRate = ((errored.length / totalFiles) * 100).toFixed(2)
|
|
176
|
-
const errorMessage =
|
|
177
|
-
|
|
160
|
+
if(errored.length > 0) {
|
|
161
|
+
// const failureRate = ((errored.length / totalFiles) * 100).toFixed(2)
|
|
162
|
+
// const errorMessage =
|
|
163
|
+
// `Errors processing ${errored.length} files [${failureRate}%]`
|
|
164
|
+
// const errorLines = errored.map(r => {
|
|
165
|
+
// const stackLine = log.lastStackLine(r.error, 0)
|
|
166
|
+
// return `\n- ${r.input.module}: ${stackLine} - ${r.error.message}`
|
|
167
|
+
// }).join("")
|
|
178
168
|
|
|
179
|
-
this.logger
|
|
169
|
+
// this.logger(errorMessage+errorLines)
|
|
180
170
|
}
|
|
181
171
|
|
|
182
172
|
debug("File processing complete", 1)
|
package/src/core/Discovery.js
CHANGED
|
@@ -5,30 +5,32 @@ import {execSync} from "child_process"
|
|
|
5
5
|
import * as FDUtil from "./util/FDUtil.js"
|
|
6
6
|
import * as ActionUtil from "./util/ActionUtil.js"
|
|
7
7
|
import * as DataUtil from "./util/DataUtil.js"
|
|
8
|
-
import * as ValidUtil from "./util/ValidUtil.js"
|
|
9
8
|
|
|
10
9
|
const {ls, resolveDirectory, resolveFilename, getFiles} = FDUtil
|
|
11
10
|
const {actionTypes, actionMetaRequirements, loadJson} = ActionUtil
|
|
12
11
|
const {isType} = DataUtil
|
|
13
|
-
const {assert} = ValidUtil
|
|
14
|
-
|
|
15
|
-
let debug
|
|
16
12
|
|
|
17
13
|
export default class Discovery {
|
|
18
14
|
#logger
|
|
15
|
+
#debug
|
|
19
16
|
|
|
20
17
|
constructor(core) {
|
|
21
18
|
this.core = core
|
|
22
19
|
this.#logger = core.logger
|
|
23
|
-
debug = this.#logger.newDebug()
|
|
20
|
+
this.#debug = this.#logger.newDebug()
|
|
24
21
|
}
|
|
25
22
|
|
|
26
23
|
/**
|
|
27
24
|
* Discover actions from local or global node_modules
|
|
28
25
|
*
|
|
26
|
+
* @param {object[]} specified The specified actions to discover
|
|
29
27
|
* @returns {Promise<object>} A map of discovered modules
|
|
30
28
|
*/
|
|
31
|
-
async discoverActions() {
|
|
29
|
+
async discoverActions({print, parse} = {}) {
|
|
30
|
+
const debug = this.#debug
|
|
31
|
+
|
|
32
|
+
debug("Discovering actions", 2)
|
|
33
|
+
|
|
32
34
|
const bucket = []
|
|
33
35
|
const options = this.core.options ?? {}
|
|
34
36
|
|
|
@@ -42,35 +44,55 @@ export default class Discovery {
|
|
|
42
44
|
])),
|
|
43
45
|
)
|
|
44
46
|
} else {
|
|
45
|
-
debug("
|
|
47
|
+
debug("Mock path not set, discovering actions in node_modules", 1)
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
debug("Looking for actions in project's package.json", 2)
|
|
50
|
+
if(this.core.packageJson?.bedoc?.modules) {
|
|
51
|
+
const actions = this.core.packageJson?.bedoc?.modules
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
debug("Found %d actions in package.json: %d", 3, actions)
|
|
54
|
+
debug("Actions found in package.json action in package.json: %o", 3, actions)
|
|
52
55
|
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
if(actions && typeof(actions) === "object")
|
|
57
|
+
bucket.push(...actions)
|
|
58
|
+
else
|
|
59
|
+
debug("No actions found in package.json", 3)
|
|
60
|
+
} else {
|
|
61
|
+
debug("No actions found in project's package.json", 2)
|
|
55
62
|
}
|
|
56
63
|
|
|
64
|
+
debug("Looking for actions in node_modules (global and locally installed", 2)
|
|
57
65
|
const directories = [
|
|
58
|
-
// "c:/temp",
|
|
59
66
|
"./node_modules",
|
|
60
67
|
execSync("npm root -g").toString().trim(),
|
|
61
68
|
]
|
|
62
69
|
|
|
70
|
+
debug("Found %d directories to search for actions", 2, directories.length)
|
|
71
|
+
debug("Directories to search for actions: %o", 3, directories)
|
|
72
|
+
|
|
63
73
|
const moduleDirectories = directories.map(resolveDirectory)
|
|
64
74
|
for(const moduleDirectory of moduleDirectories) {
|
|
65
75
|
const {directories: dirs} = await ls(moduleDirectory.absolutePath)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
|
|
77
|
+
debug("Found %d directories in `%s`", 2,
|
|
78
|
+
dirs.length, moduleDirectory.absolutePath
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
const bedocDirs = dirs.filter(d => d.name.startsWith("bedoc-"))
|
|
82
|
+
debug("Found %d bedoc directories under %s", 2, bedocDirs.length, moduleDirectory.absolutePath)
|
|
83
|
+
|
|
84
|
+
const exports = bedocDirs.map(d => this.#getModuleExports(d))
|
|
85
|
+
debug("Found %d module exports under %s", 2, exports.length, moduleDirectory.absolutePath)
|
|
86
|
+
|
|
69
87
|
bucket.push(...exports.flat())
|
|
70
88
|
}
|
|
71
89
|
}
|
|
72
90
|
|
|
73
|
-
|
|
91
|
+
debug("Discovered %d actions", 2, bucket.length)
|
|
92
|
+
|
|
93
|
+
return await this.#loadActionsAndContracts(
|
|
94
|
+
bucket, {print: print, parse: parse}
|
|
95
|
+
)
|
|
74
96
|
}
|
|
75
97
|
|
|
76
98
|
/**
|
|
@@ -80,11 +102,18 @@ export default class Discovery {
|
|
|
80
102
|
* @returns {object[]} The discovered module exports
|
|
81
103
|
*/
|
|
82
104
|
#getModuleExports(dirMap) {
|
|
105
|
+
const debug = this.#debug
|
|
106
|
+
debug("Getting module exports from `%s`", 3, dirMap.absolutePath)
|
|
107
|
+
|
|
83
108
|
const packageJsonFile = resolveFilename("package.json", dirMap)
|
|
109
|
+
debug("Loading package.json from `%s`", 3, packageJsonFile.absolutePath)
|
|
110
|
+
|
|
84
111
|
const packageJson = loadJson(packageJsonFile)
|
|
112
|
+
debug("Loaded package.json from `%s`", 3, packageJsonFile.absolutePath)
|
|
113
|
+
|
|
85
114
|
const bedocPackageJsonModules = packageJson.bedoc?.modules ?? []
|
|
86
|
-
const bedocModuleFiles = bedocPackageJsonModules.map(
|
|
87
|
-
resolveFilename(file, dirMap)
|
|
115
|
+
const bedocModuleFiles = bedocPackageJsonModules.map(file =>
|
|
116
|
+
resolveFilename(file, dirMap)
|
|
88
117
|
)
|
|
89
118
|
|
|
90
119
|
return bedocModuleFiles
|
|
@@ -95,68 +124,114 @@ export default class Discovery {
|
|
|
95
124
|
* respective contracts.
|
|
96
125
|
*
|
|
97
126
|
* @param {object[]} moduleFiles The module file objects to process
|
|
127
|
+
* @param {object} specific The specific actions to load
|
|
98
128
|
* @returns {Promise<object>} The discovered action
|
|
99
129
|
*/
|
|
100
|
-
async #loadActionsAndContracts(moduleFiles) {
|
|
130
|
+
async #loadActionsAndContracts(moduleFiles, specific) {
|
|
131
|
+
const debug = this.#debug
|
|
132
|
+
|
|
133
|
+
debug("Loading actions and contracts", 2)
|
|
134
|
+
debug("Loading %d module files", 2, moduleFiles.length)
|
|
135
|
+
debug("Specific actions to load: %o", 2, specific)
|
|
136
|
+
|
|
101
137
|
const resultActions = {}
|
|
138
|
+
actionTypes.forEach(actionType => (resultActions[actionType] = []))
|
|
139
|
+
|
|
140
|
+
// Tag the specific actions to load, so we can filter them later
|
|
141
|
+
for(const [type, file] of Object.entries(specific)) {
|
|
142
|
+
if(file) {
|
|
143
|
+
debug("Tagging specific action `%s` as `%s`", 3, file.absolutePath, type)
|
|
144
|
+
file.specificType = type
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const toLoad = [
|
|
149
|
+
...moduleFiles,
|
|
150
|
+
...Object.values(specific).filter(Boolean),
|
|
151
|
+
]
|
|
102
152
|
|
|
103
|
-
|
|
153
|
+
debug("Loading %d combined actions", 2, toLoad.length)
|
|
154
|
+
debug("Actions to load: %o", 3, toLoad)
|
|
104
155
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
156
|
+
const loadedActions = []
|
|
157
|
+
for(const file of toLoad) {
|
|
158
|
+
debug("Loading module `%s`", 2, file.absolutePath)
|
|
108
159
|
|
|
109
|
-
|
|
110
|
-
|
|
160
|
+
const loading = await this.#loadModule(file)
|
|
161
|
+
const loaded = loading.actions.map((action, index) => {
|
|
162
|
+
const contract = yaml.parse(loading.contracts[index])
|
|
163
|
+
|
|
164
|
+
return {file, action, contract}
|
|
165
|
+
})
|
|
166
|
+
loadedActions.push(...loaded)
|
|
167
|
+
}
|
|
111
168
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
169
|
+
debug("Loaded %d actions", 2, loadedActions.length)
|
|
170
|
+
|
|
171
|
+
const filtered = []
|
|
172
|
+
for(const actionType of actionTypes) {
|
|
173
|
+
const file = specific[actionType]
|
|
174
|
+
const matchingActions = []
|
|
175
|
+
if(file) {
|
|
176
|
+
debug("Filtering actions for specific `%s`", 2, actionType)
|
|
177
|
+
const found = loadedActions.find(
|
|
178
|
+
e => e.file.absolutePath === file.absolutePath
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if(!found)
|
|
182
|
+
throw new Error(`Could not find specific action: ${file.absolutePath}`)
|
|
183
|
+
|
|
184
|
+
matchingActions.push(found)
|
|
185
|
+
} else {
|
|
186
|
+
debug("No specific action required for `%s`", 2, actionType)
|
|
187
|
+
|
|
188
|
+
const found = loadedActions.filter(
|
|
189
|
+
e => e.action.meta.action === actionType
|
|
190
|
+
)
|
|
191
|
+
matchingActions.push(...found)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
debug("Filtered %d actions for `%s`", 2,
|
|
195
|
+
matchingActions.length, actionType
|
|
116
196
|
)
|
|
117
197
|
|
|
118
|
-
|
|
198
|
+
filtered.push(...matchingActions)
|
|
199
|
+
}
|
|
119
200
|
|
|
120
|
-
|
|
121
|
-
const tempContract = contracts[i]
|
|
201
|
+
debug("Filtered %d actions", 2, filtered.length)
|
|
122
202
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
203
|
+
// Now check the metas for validity
|
|
204
|
+
for(const e of filtered) {
|
|
205
|
+
const {action, contract, file: moduleFile} = e
|
|
206
|
+
const meta = action.meta
|
|
207
|
+
if(!meta)
|
|
208
|
+
throw new TypeError("Action has no meta object:\n" +
|
|
209
|
+
JSON.stringify(moduleFile, null, 2) + "\n" +
|
|
210
|
+
JSON.stringify(action, null, 2))
|
|
129
211
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
212
|
+
const metaAction = meta.action
|
|
213
|
+
if(!metaAction)
|
|
214
|
+
throw new TypeError("Action has no meta action:\n" +
|
|
215
|
+
JSON.stringify(moduleFile, null, 2) + "\n" +
|
|
216
|
+
JSON.stringify(action, null, 2))
|
|
135
217
|
|
|
136
|
-
|
|
137
|
-
const metaAction = meta?.action
|
|
218
|
+
debug("Checking action `%s`", 2, metaAction)
|
|
138
219
|
|
|
139
|
-
|
|
220
|
+
const isValid = this.#validMeta(metaAction, {action, contract})
|
|
140
221
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
222
|
+
debug("Action `%o` in `%s` is %s", 3,
|
|
223
|
+
metaAction, moduleFile.module, isValid ? "valid" : "invalid"
|
|
224
|
+
)
|
|
144
225
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
result.accepted++
|
|
148
|
-
resultActions[actionType].push(curr)
|
|
149
|
-
continue
|
|
150
|
-
} else {
|
|
151
|
-
debug("Action is not a valid `%s` action", 3, actionType)
|
|
152
|
-
}
|
|
153
|
-
}
|
|
226
|
+
if(isValid) {
|
|
227
|
+
debug("Action is a valid `%s` action", 3, metaAction)
|
|
154
228
|
|
|
155
|
-
|
|
156
|
-
|
|
229
|
+
resultActions[metaAction].push({file: moduleFile, action, contract})
|
|
230
|
+
} else {
|
|
231
|
+
debug("Action is not a valid `%s` action", 3, metaAction)
|
|
157
232
|
}
|
|
158
233
|
|
|
159
|
-
debug("Processed
|
|
234
|
+
debug("Processed action `%s`", 2, metaAction)
|
|
160
235
|
}
|
|
161
236
|
|
|
162
237
|
for(const actionType of actionTypes) {
|
|
@@ -168,13 +243,92 @@ export default class Discovery {
|
|
|
168
243
|
return acc + resultActions[curr].length
|
|
169
244
|
}, 0)
|
|
170
245
|
|
|
171
|
-
debug("Loaded %d action definitions from %d modules", 2,
|
|
246
|
+
debug("Loaded %d action definitions from %d modules", 2,
|
|
247
|
+
total, moduleFiles.length
|
|
248
|
+
)
|
|
172
249
|
|
|
173
250
|
return resultActions
|
|
174
251
|
}
|
|
175
252
|
|
|
176
|
-
|
|
253
|
+
satisfyCriteria(actions, validatedConfig) {
|
|
254
|
+
const debug = this.#debug
|
|
255
|
+
const satisfied = {parse: [], print: []}
|
|
256
|
+
const toMatch = {
|
|
257
|
+
parse: {criterion: "language", config: "parser"},
|
|
258
|
+
print: {criterion: "format", config: "printer"}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
debug("Satisfying criteria for actions", 2)
|
|
262
|
+
for(const [actionType, search] of Object.entries(toMatch)) {
|
|
263
|
+
debug("Satisfying criteria for `%s` actions", 2, actionType)
|
|
264
|
+
|
|
265
|
+
const {criterion, config} = search
|
|
266
|
+
debug("Criterion: %s, Config: %s", 3, criterion, config)
|
|
267
|
+
|
|
268
|
+
// First let's check if we wanted something specific
|
|
269
|
+
if(validatedConfig[config]) {
|
|
270
|
+
debug("Checking for specific `%s` action", 3, actionType)
|
|
271
|
+
const found = actions[actionType].find(
|
|
272
|
+
a => a.file.specificType === actionType
|
|
273
|
+
)
|
|
274
|
+
if(found) {
|
|
275
|
+
debug("Found specific `%s` action", 3, actionType)
|
|
276
|
+
satisfied[actionType].push(found)
|
|
277
|
+
continue
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
debug("No specific `%s` action found", 3, actionType)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Hmm! We didn't find anything specific. Let's check the criterion
|
|
284
|
+
debug("Checking for `%s` actions with criterion `%s`", 3, actionType, criterion)
|
|
285
|
+
const found = actions[actionType].filter(a => {
|
|
286
|
+
debug("Meta criterion value: %o", 4, a.action.meta[criterion])
|
|
287
|
+
debug("Config criterion value: %o", 4, validatedConfig[criterion])
|
|
288
|
+
return a.action.meta[criterion] === validatedConfig[criterion]
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
debug("Found %d `%s` actions with criterion `%s`", 3,
|
|
292
|
+
found.length, actionType, criterion
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
// Shove them into the result!
|
|
296
|
+
satisfied[actionType].push(...found)
|
|
297
|
+
|
|
298
|
+
// That should about cover it!
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return satisfied
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Load a module and return its exports
|
|
306
|
+
*
|
|
307
|
+
* @param {object} module The module object to load
|
|
308
|
+
* @returns {Promise<object>} The module exports {actions, contracts}
|
|
309
|
+
*/
|
|
310
|
+
async #loadModule(module) {
|
|
311
|
+
const debug = this.#debug
|
|
312
|
+
|
|
313
|
+
debug("Loading module `%j`", 2, module)
|
|
314
|
+
|
|
315
|
+
const {absoluteUri} = module
|
|
316
|
+
const moduleExports = await import(absoluteUri)
|
|
317
|
+
|
|
318
|
+
return {file: module, ...moduleExports}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Validates the meta requirements for an action
|
|
323
|
+
*
|
|
324
|
+
* @param {string} actionType The action type to validate
|
|
325
|
+
* @param {object} toValidate - The action object to validate
|
|
326
|
+
* @returns {boolean} Whether the action object meets the meta requirements
|
|
327
|
+
*/
|
|
328
|
+
#validMeta(actionType, toValidate) {
|
|
329
|
+
const debug = this.#debug
|
|
177
330
|
debug("Checking meta requirements for `%s`", 3, actionType)
|
|
331
|
+
|
|
178
332
|
const requirements = actionMetaRequirements[actionType]
|
|
179
333
|
if(!requirements)
|
|
180
334
|
throw new Error(
|
|
@@ -8,17 +8,17 @@ const {assert} = ValidUtil
|
|
|
8
8
|
const freeze = Object.freeze
|
|
9
9
|
|
|
10
10
|
const hookEvents = freeze(["start", "section_load", "enter", "exit", "end"])
|
|
11
|
-
const hookPoints = freeze(
|
|
11
|
+
export const hookPoints = freeze(
|
|
12
12
|
await allocateObject(
|
|
13
13
|
hookEvents.map((event) => event.toUpperCase()),
|
|
14
14
|
hookEvents,
|
|
15
15
|
),
|
|
16
16
|
)
|
|
17
17
|
|
|
18
|
-
class
|
|
18
|
+
export default class HookManager {
|
|
19
19
|
#hooksFile = null
|
|
20
20
|
#log = null
|
|
21
|
-
#hooks =
|
|
21
|
+
#hooks = null
|
|
22
22
|
#action = null
|
|
23
23
|
#timeout = 1
|
|
24
24
|
|
|
@@ -49,11 +49,19 @@ class HooksManager {
|
|
|
49
49
|
return this.#timeout
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
get setup() {
|
|
53
|
+
return this.hooks?.setup || null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get cleanup() {
|
|
57
|
+
return this.hooks?.cleanup || null
|
|
58
|
+
}
|
|
59
|
+
|
|
52
60
|
static async new(arg) {
|
|
53
|
-
const instance = new
|
|
61
|
+
const instance = new HookManager(arg)
|
|
54
62
|
const debug = instance.log.newDebug()
|
|
55
63
|
|
|
56
|
-
debug("Creating new
|
|
64
|
+
debug("Creating new HookManager instance with args: `%o`", 2, arg)
|
|
57
65
|
|
|
58
66
|
const hooksFile = instance.hooksFile
|
|
59
67
|
|
|
@@ -116,7 +124,7 @@ class HooksManager {
|
|
|
116
124
|
1,
|
|
117
125
|
)
|
|
118
126
|
|
|
119
|
-
const hookExecution = await hook.call(this, ...args)
|
|
127
|
+
const hookExecution = await hook.call(this.hooks, ...args)
|
|
120
128
|
const hookTimeout = this.parent.timeout
|
|
121
129
|
const expireAsync = () =>
|
|
122
130
|
timeoutPromise(
|
|
@@ -134,10 +142,3 @@ class HooksManager {
|
|
|
134
142
|
}
|
|
135
143
|
}
|
|
136
144
|
}
|
|
137
|
-
|
|
138
|
-
export {
|
|
139
|
-
// Class
|
|
140
|
-
HooksManager,
|
|
141
|
-
// Constants
|
|
142
|
-
hookPoints,
|
|
143
|
-
}
|
package/src/core/Logger.js
CHANGED
|
@@ -119,8 +119,8 @@ export default class Logger {
|
|
|
119
119
|
return `[${this.#name}] ${loggerColours[level]}${tag}${loggerColours.reset}: ${message}`
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
lastStackLine(stepsRemoved = 3) {
|
|
123
|
-
const stack = ErrorStackParser.parse(
|
|
122
|
+
lastStackLine(error = new Error(), stepsRemoved = 3) {
|
|
123
|
+
const stack = ErrorStackParser.parse(error)
|
|
124
124
|
return stack[stepsRemoved]
|
|
125
125
|
}
|
|
126
126
|
|
|
@@ -4,23 +4,4 @@ export default class ParseManager extends ActionManager {
|
|
|
4
4
|
constructor(actionDefinition, logger) {
|
|
5
5
|
super(actionDefinition, logger)
|
|
6
6
|
}
|
|
7
|
-
|
|
8
|
-
async parse(fileMap, content) {
|
|
9
|
-
const log = this.log
|
|
10
|
-
const debug = log.newDebug()
|
|
11
|
-
|
|
12
|
-
debug("Parsing file `%j`", 3, fileMap)
|
|
13
|
-
|
|
14
|
-
if(this.action.init)
|
|
15
|
-
this.action.init({parent: this, log})
|
|
16
|
-
|
|
17
|
-
if(!this.action.parse)
|
|
18
|
-
throw new Error(`No parse function found for action: ${this.module}`)
|
|
19
|
-
|
|
20
|
-
const result = await this.action.parse(fileMap.path, content)
|
|
21
|
-
|
|
22
|
-
debug("Parse complete of file `%j`", 3, fileMap)
|
|
23
|
-
|
|
24
|
-
return result
|
|
25
|
-
}
|
|
26
7
|
}
|
|
@@ -4,23 +4,4 @@ export default class PrintManager extends ActionManager {
|
|
|
4
4
|
constructor(actionDefinition, logger) {
|
|
5
5
|
super(actionDefinition, logger)
|
|
6
6
|
}
|
|
7
|
-
|
|
8
|
-
async print(fileMap, content) {
|
|
9
|
-
const log = this.log
|
|
10
|
-
const debug = log.newDebug()
|
|
11
|
-
|
|
12
|
-
debug("Printing data for `%s`", 3, fileMap.module)
|
|
13
|
-
|
|
14
|
-
if(this.action.init)
|
|
15
|
-
this.action.init({parent: this, log})
|
|
16
|
-
|
|
17
|
-
if(!this.action.print)
|
|
18
|
-
throw new Error(`No print function found for action: ${this.module}`)
|
|
19
|
-
|
|
20
|
-
const result = await this.action.print(fileMap.module, content)
|
|
21
|
-
|
|
22
|
-
debug("Print complete for `%s`", 3, fileMap.module)
|
|
23
|
-
|
|
24
|
-
return result
|
|
25
|
-
}
|
|
26
7
|
}
|