adapt-project 1.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/.github/workflows/releases.yml +32 -0
- package/.github/workflows/standardjs.yml +13 -0
- package/.github/workflows/tests.yml +13 -0
- package/README.md +168 -0
- package/eslint.config.js +10 -0
- package/index.js +13 -0
- package/lib/Data.js +185 -0
- package/lib/Framework.js +295 -0
- package/lib/JSONFile.js +104 -0
- package/lib/JSONFileItem.js +27 -0
- package/lib/Plugins.js +82 -0
- package/lib/Schemas.js +208 -0
- package/lib/Translate.js +495 -0
- package/lib/data/Language.js +301 -0
- package/lib/data/LanguageFile.js +35 -0
- package/lib/plugins/Plugin.js +143 -0
- package/lib/schema/ExtensionSchema.js +26 -0
- package/lib/schema/GlobalsSchema.js +65 -0
- package/lib/schema/ModelSchema.js +45 -0
- package/lib/schema/ModelSchemas.js +41 -0
- package/lib/schema/Schema.js +191 -0
- package/package.json +54 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import fs from 'fs-extra'
|
|
3
|
+
import globs from 'globs'
|
|
4
|
+
import _ from 'lodash'
|
|
5
|
+
import chalk from 'chalk'
|
|
6
|
+
import LanguageFile from './LanguageFile.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {import('../Framework')} Framework
|
|
10
|
+
* @typedef {import('../JSONFileItem')} JSONFileItem
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Represents all of the json files and file item in a language directory
|
|
15
|
+
* at course/[lang]/*.[jsonext].
|
|
16
|
+
* It is filename agnostic, such that there are no hard references to file names.
|
|
17
|
+
* It has _id and _parentId structure checking and _trackingId management included.
|
|
18
|
+
*/
|
|
19
|
+
class Language {
|
|
20
|
+
/**
|
|
21
|
+
* @param {Object} options
|
|
22
|
+
* @param {Framework} options.framework
|
|
23
|
+
* @param {string} options.languagePath
|
|
24
|
+
* @param {string} options.jsonext
|
|
25
|
+
* @param {string} options.trackingIdType,
|
|
26
|
+
* @param {function} options.log
|
|
27
|
+
*/
|
|
28
|
+
constructor ({
|
|
29
|
+
framework = null,
|
|
30
|
+
languagePath = '',
|
|
31
|
+
jsonext = 'json',
|
|
32
|
+
trackingIdType = 'block',
|
|
33
|
+
log = console.log
|
|
34
|
+
} = {}) {
|
|
35
|
+
/** @type {Framework} */
|
|
36
|
+
this.framework = framework
|
|
37
|
+
/** @type {string} */
|
|
38
|
+
this.jsonext = jsonext
|
|
39
|
+
/** @type {string} */
|
|
40
|
+
this.path = path.normalize(languagePath).replace(/\\/g, '/')
|
|
41
|
+
/** @type {string} */
|
|
42
|
+
this.name = this.path.split('/').filter(Boolean).pop()
|
|
43
|
+
/** @type {string} */
|
|
44
|
+
this.rootPath = this.path.split('/').filter(Boolean).slice(0, -1).join('/')
|
|
45
|
+
/** @type {string} */
|
|
46
|
+
this.manifestFileName = 'language_data_manifest.js'
|
|
47
|
+
/** @type {string} */
|
|
48
|
+
this.manifestPath = this.path + this.manifestFileName
|
|
49
|
+
/** @type {string} */
|
|
50
|
+
this.trackingIdType = trackingIdType
|
|
51
|
+
/** @type {[LanguageFile]} */
|
|
52
|
+
this.files = null
|
|
53
|
+
/** @type {Object.<string, JSONFileItem>} */
|
|
54
|
+
this._itemIdIndex = null
|
|
55
|
+
/** @type {[JSONFileItem]} */
|
|
56
|
+
this.courseFileItem = null
|
|
57
|
+
/** @type {function} */
|
|
58
|
+
this.log = log
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** @returns {Language} */
|
|
62
|
+
load () {
|
|
63
|
+
this.files = []
|
|
64
|
+
this._itemIdIndex = {}
|
|
65
|
+
this.courseFileItem = null
|
|
66
|
+
|
|
67
|
+
const dataFiles = globs.sync(path.join(this.path, '*.' + this.jsonext)).map((dataFilePath) => {
|
|
68
|
+
const relativePath = dataFilePath.slice(this.path.length)
|
|
69
|
+
return relativePath
|
|
70
|
+
}).filter((dataFilePath) => {
|
|
71
|
+
const isManifest = (dataFilePath === this.manifestPath)
|
|
72
|
+
// Skip file if it is the Authoring Tool import/export asset manifest
|
|
73
|
+
const isAATAssetJSON = (dataFilePath === 'assets.json')
|
|
74
|
+
return !isManifest && !isAATAssetJSON
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
dataFiles.forEach(jsonFileName => {
|
|
78
|
+
const jsonFilePath = (this.path + jsonFileName).replace(/\\/g, '/')
|
|
79
|
+
const file = new LanguageFile({
|
|
80
|
+
framework: this.framework,
|
|
81
|
+
language: this,
|
|
82
|
+
jsonext: this.jsonext,
|
|
83
|
+
path: jsonFilePath,
|
|
84
|
+
data: null,
|
|
85
|
+
hasChanged: false
|
|
86
|
+
})
|
|
87
|
+
file.load()
|
|
88
|
+
this.files.push(file)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
this._itemIdIndex = this.getAllFileItems().reduce((index, fileItem) => {
|
|
92
|
+
const { file, item } = fileItem
|
|
93
|
+
if (item._id && index[item._id]) {
|
|
94
|
+
const err = new Error(`Duplicate ids ${item._id} in ${index[item._id].file.path} and ${file.path}`)
|
|
95
|
+
err.number = 10006
|
|
96
|
+
throw err
|
|
97
|
+
} else if (item._id) {
|
|
98
|
+
index[item._id] = fileItem
|
|
99
|
+
}
|
|
100
|
+
if (item._type === 'course' && this.courseFileItem) {
|
|
101
|
+
const err = new Error(`Duplicate course items found, in ${index[item._id].file.path} and ${file.path}`)
|
|
102
|
+
err.number = 10007
|
|
103
|
+
throw err
|
|
104
|
+
} else if (item._type === 'course') {
|
|
105
|
+
this.courseFileItem = fileItem
|
|
106
|
+
}
|
|
107
|
+
return index
|
|
108
|
+
}, {})
|
|
109
|
+
|
|
110
|
+
return this
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** @type {boolean} */
|
|
114
|
+
get isValid () {
|
|
115
|
+
return Boolean(this.courseFileItem)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** @type {boolean} */
|
|
119
|
+
get hasChanged () {
|
|
120
|
+
return this.files.some(file => file.hasChanged)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Produces a manifest file for the Framework data layer at course/lang/language_data_manifest.js.
|
|
125
|
+
* @returns {Language}
|
|
126
|
+
*/
|
|
127
|
+
saveManifest () {
|
|
128
|
+
const dataFiles = globs.sync(path.join(this.path, '*.' + this.jsonext)).map((dataFilePath) => {
|
|
129
|
+
const relativePath = dataFilePath.slice(this.path.length)
|
|
130
|
+
return relativePath
|
|
131
|
+
}).filter((dataFilePath) => {
|
|
132
|
+
const isManifest = (dataFilePath === this.manifestPath)
|
|
133
|
+
// Skip file if it is the Authoring Tool import/export asset manifest
|
|
134
|
+
const isAATAssetJSON = (dataFilePath === 'assets.json')
|
|
135
|
+
return !isManifest && !isAATAssetJSON
|
|
136
|
+
})
|
|
137
|
+
const hasNoDataFiles = !dataFiles.length
|
|
138
|
+
if (hasNoDataFiles) {
|
|
139
|
+
const err = new Error(`No data files found in ${this.path}`)
|
|
140
|
+
err.number = 10008
|
|
141
|
+
throw err
|
|
142
|
+
}
|
|
143
|
+
fs.writeJSONSync(this.manifestPath, dataFiles, { spaces: 0 })
|
|
144
|
+
return this
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** @returns {[JSONFileItem]} */
|
|
148
|
+
getAllFileItems () {
|
|
149
|
+
return this.files.reduce((memo, file) => {
|
|
150
|
+
memo.push(...file.fileItems)
|
|
151
|
+
return memo
|
|
152
|
+
}, [])
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** @returns {JSONFileItem} */
|
|
156
|
+
getCourseFileItem () {
|
|
157
|
+
if (!this.courseFileItem) {
|
|
158
|
+
const err = new Error(`Could not find course item for ${this.path}`)
|
|
159
|
+
err.number = 10009
|
|
160
|
+
throw err
|
|
161
|
+
}
|
|
162
|
+
return this.courseFileItem
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @param {string} id
|
|
167
|
+
* @returns {JSONFileItem}
|
|
168
|
+
*/
|
|
169
|
+
getFileItemById (id) {
|
|
170
|
+
const fileItem = this._itemIdIndex[id]
|
|
171
|
+
if (!fileItem) {
|
|
172
|
+
const err = new Error(`Could not find item for id ${id} in ${this.path}`)
|
|
173
|
+
err.number = 10010
|
|
174
|
+
throw err
|
|
175
|
+
}
|
|
176
|
+
return fileItem
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** @returns {Language} */
|
|
180
|
+
checkIds () {
|
|
181
|
+
const items = this.getAllFileItems().map(({ item }) => item)
|
|
182
|
+
// Index and group
|
|
183
|
+
const idIndex = _.keyBy(items, '_id')
|
|
184
|
+
const idGroups = _.groupBy(items, '_id')
|
|
185
|
+
const parentIdGroups = _.groupBy(items, '_parentId')
|
|
186
|
+
// Setup error collection arrays
|
|
187
|
+
const orphanedIds = {}
|
|
188
|
+
let emptyIds = {}
|
|
189
|
+
let duplicateIds = {}
|
|
190
|
+
const missingIds = {}
|
|
191
|
+
items.forEach((o) => {
|
|
192
|
+
const isCourseType = (o._type === 'course')
|
|
193
|
+
const isComponentType = (o._type === 'component')
|
|
194
|
+
if (idGroups[o._id].length > 1) {
|
|
195
|
+
duplicateIds[o._id] = true // Id has more than one item
|
|
196
|
+
}
|
|
197
|
+
if (!isComponentType && !parentIdGroups[o._id]) {
|
|
198
|
+
emptyIds[o._id] = true // Course has no children
|
|
199
|
+
}
|
|
200
|
+
if (!isCourseType && (!o._parentId || !idIndex[o._parentId])) {
|
|
201
|
+
orphanedIds[o._id] = o._type // Item has no defined parent id or the parent id doesn't exist
|
|
202
|
+
}
|
|
203
|
+
if (!isCourseType && o._parentId && !idIndex[o._parentId]) {
|
|
204
|
+
missingIds[o._parentId] = o._type // Referenced parent item does not exist
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
const orphanedIdsArray = Object.keys(orphanedIds)
|
|
208
|
+
const missingIdsArray = Object.keys(missingIds)
|
|
209
|
+
emptyIds = Object.keys(emptyIds)
|
|
210
|
+
duplicateIds = Object.keys(duplicateIds)
|
|
211
|
+
// Output for each type of error
|
|
212
|
+
const hasErrored = orphanedIdsArray.length || emptyIds.length || duplicateIds.length || missingIdsArray.length
|
|
213
|
+
if (orphanedIdsArray.length) {
|
|
214
|
+
const orphanedIdString = orphanedIdsArray.map((id) => `${id} (${orphanedIds[id]})`)
|
|
215
|
+
this.log(chalk.yellow(`Orphaned _ids: ${orphanedIdString.join(', ')}`))
|
|
216
|
+
}
|
|
217
|
+
if (missingIdsArray.length) {
|
|
218
|
+
const missingIdString = missingIdsArray.map((id) => `${id} (${missingIds[id]})`)
|
|
219
|
+
this.log(chalk.yellow(`Missing _ids: ${missingIdString.join(', ')}`))
|
|
220
|
+
}
|
|
221
|
+
if (emptyIds.length) {
|
|
222
|
+
this.log(chalk.yellow(`Empty _ids: ${emptyIds}`))
|
|
223
|
+
}
|
|
224
|
+
if (duplicateIds.length) {
|
|
225
|
+
this.log(chalk.yellow(`Duplicate _ids: ${duplicateIds}`))
|
|
226
|
+
}
|
|
227
|
+
// If any error has occured, stop processing.
|
|
228
|
+
if (hasErrored) {
|
|
229
|
+
const err = new Error('Oops, looks like you have some json errors.')
|
|
230
|
+
err.number = 10011
|
|
231
|
+
throw err
|
|
232
|
+
}
|
|
233
|
+
this.log(`No issues found in course/${this.name}, your JSON is a-ok!`)
|
|
234
|
+
|
|
235
|
+
return this
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** @returns {Language} */
|
|
239
|
+
addTrackingIds () {
|
|
240
|
+
const { file, item: course } = this.getCourseFileItem()
|
|
241
|
+
course._latestTrackingId = course._latestTrackingId || -1
|
|
242
|
+
file.changed()
|
|
243
|
+
|
|
244
|
+
let wasAdded = false
|
|
245
|
+
const trackingIdsSeen = []
|
|
246
|
+
const fileItems = this.getAllFileItems().filter(fileItem => fileItem.item._type === this.trackingIdType)
|
|
247
|
+
fileItems.forEach(({ file, item }) => {
|
|
248
|
+
this.log(`${this.trackingIdType}: ${item._id}: ${item._trackingId !== undefined ? item._trackingId : 'not set'}`)
|
|
249
|
+
if (item._trackingId === undefined) {
|
|
250
|
+
item._trackingId = ++course._latestTrackingId
|
|
251
|
+
file.changed()
|
|
252
|
+
wasAdded = true
|
|
253
|
+
this.log(`Adding tracking ID: ${item._trackingId} to ${this.trackingIdType} ${item._id}`)
|
|
254
|
+
} else {
|
|
255
|
+
if (trackingIdsSeen.indexOf(item._trackingId) > -1) {
|
|
256
|
+
item._trackingId = ++course._latestTrackingId
|
|
257
|
+
file.changed()
|
|
258
|
+
wasAdded = true
|
|
259
|
+
this.log(`Warning: ${item._id} has the tracking ID ${item._trackingId} but this is already in use. Changing to ${course._latestTrackingId + 1}.`)
|
|
260
|
+
} else {
|
|
261
|
+
trackingIdsSeen.push(item._trackingId)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (course._latestTrackingId < item._trackingId) {
|
|
265
|
+
course._latestTrackingId = item._trackingId
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
this.save()
|
|
270
|
+
this.log(`Tracking IDs ${wasAdded ? 'were added to' : 'are ok for'} course/${this.name}. The latest tracking ID is ${course._latestTrackingId}\n`)
|
|
271
|
+
|
|
272
|
+
return this
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** @returns {Language} */
|
|
276
|
+
removeTrackingIds () {
|
|
277
|
+
const { file, item: course } = this.getCourseFileItem()
|
|
278
|
+
course._latestTrackingId = -1
|
|
279
|
+
file.changed()
|
|
280
|
+
|
|
281
|
+
this.getAllFileItems().forEach(({ file, item }) => {
|
|
282
|
+
if (item._type !== this.trackingIdType) return
|
|
283
|
+
delete item._trackingId
|
|
284
|
+
file.changed()
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
this.save()
|
|
288
|
+
this.log(`Tracking IDs removed from course/${this.name}.`)
|
|
289
|
+
|
|
290
|
+
return this
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** @returns {Language} */
|
|
294
|
+
save () {
|
|
295
|
+
this.files.forEach(file => file.save())
|
|
296
|
+
this.load()
|
|
297
|
+
return this
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export default Language
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import JSONFile from '../JSONFile.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import('../Framework')} Framework
|
|
5
|
+
* @typedef {import('./Language')} Language
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Represents any of the files at course/[lang]/*.[jsonext].
|
|
10
|
+
*/
|
|
11
|
+
class LanguageFile extends JSONFile {
|
|
12
|
+
/**
|
|
13
|
+
* @param {Object} options
|
|
14
|
+
* @param {Framework} options.framework
|
|
15
|
+
* @param {Language} options.language
|
|
16
|
+
* @param {string} options.path
|
|
17
|
+
* @param {string} options.jsonext
|
|
18
|
+
* @param {Object} options.data
|
|
19
|
+
* @param {boolean} options.hasChanged
|
|
20
|
+
*/
|
|
21
|
+
constructor ({
|
|
22
|
+
framework = null,
|
|
23
|
+
language = null,
|
|
24
|
+
path = null,
|
|
25
|
+
jsonext = null,
|
|
26
|
+
data = null,
|
|
27
|
+
hasChanged = false
|
|
28
|
+
} = {}) {
|
|
29
|
+
super({ framework, path, jsonext, data, hasChanged })
|
|
30
|
+
/** @type {Language} */
|
|
31
|
+
this.language = language
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default LanguageFile
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import semver from 'semver'
|
|
2
|
+
import globs from 'globs'
|
|
3
|
+
import JSONFile from '../JSONFile.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Represents a single plugin location, bower.json, version, name and schema
|
|
7
|
+
* locations.
|
|
8
|
+
* @todo Should be able to define multiple schemas for all plugins in the AAT
|
|
9
|
+
* and in the Framework
|
|
10
|
+
* @todo Switch to package.json with the move to npm
|
|
11
|
+
* @todo Plugin type is inferred from the JSON.
|
|
12
|
+
* @todo Component _globals target attribute is inferred from the _component
|
|
13
|
+
*/
|
|
14
|
+
class Plugin {
|
|
15
|
+
/**
|
|
16
|
+
* @param {Object} options
|
|
17
|
+
* @param {Framework} options.framework
|
|
18
|
+
* @param {string} options.sourcePath
|
|
19
|
+
* @param {string} options.jsonext
|
|
20
|
+
* @param {function} options.log
|
|
21
|
+
*/
|
|
22
|
+
constructor ({
|
|
23
|
+
framework = null,
|
|
24
|
+
sourcePath = '',
|
|
25
|
+
log = console.log,
|
|
26
|
+
warn = console.warn
|
|
27
|
+
} = {}) {
|
|
28
|
+
/** @type {Framework} */
|
|
29
|
+
this.framework = framework
|
|
30
|
+
/** @type {string} */
|
|
31
|
+
this.sourcePath = sourcePath.replace(/\\/g, '/')
|
|
32
|
+
/** @type {function} */
|
|
33
|
+
this.log = log
|
|
34
|
+
/** @type {function} */
|
|
35
|
+
this.warn = warn
|
|
36
|
+
/** @type {JSONFile} */
|
|
37
|
+
this.packageJSONFile = null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @returns {Plugin} */
|
|
41
|
+
load () {
|
|
42
|
+
const pathDerivedName = this.sourcePath.split('/').filter(Boolean).pop()
|
|
43
|
+
const files = globs.sync(this.packageJSONLocations)
|
|
44
|
+
const firstFile = files[0]
|
|
45
|
+
if (firstFile) {
|
|
46
|
+
// use the first package definition found (this will be bower.json / package.json)
|
|
47
|
+
this.packageJSONFile = new JSONFile({
|
|
48
|
+
path: firstFile
|
|
49
|
+
})
|
|
50
|
+
} else {
|
|
51
|
+
// no json found, make some up (this will usually be the core)
|
|
52
|
+
this.packageJSONFile = new JSONFile({
|
|
53
|
+
path: null,
|
|
54
|
+
data: {
|
|
55
|
+
name: pathDerivedName
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
this.packageJSONFile.load()
|
|
60
|
+
const packageName = (this.packageJSONFile.firstFileItem.item.name)
|
|
61
|
+
if (packageName !== pathDerivedName) {
|
|
62
|
+
// assume path name is also plugin name, this shouldn't be necessary
|
|
63
|
+
this.warn(`Plugin folder name ${pathDerivedName} does not match package name ${packageName}.`)
|
|
64
|
+
}
|
|
65
|
+
if (this.requiredFramework && !this.isFrameworkCompatible) {
|
|
66
|
+
this.warn(`Required framework version (${this.requiredFramework}) for plugin ${packageName} not satisfied by current framework version (${this.framework.version}).`)
|
|
67
|
+
}
|
|
68
|
+
return this
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Informs the Schemas API from where to fetch the schemas defined in this
|
|
73
|
+
* plugin.
|
|
74
|
+
* @returns {[string]}
|
|
75
|
+
*/
|
|
76
|
+
get schemaLocations () {
|
|
77
|
+
return [
|
|
78
|
+
`${this.sourcePath}properties.schema`,
|
|
79
|
+
`${this.sourcePath}schema/*.schema`
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @returns {[string]}
|
|
85
|
+
*/
|
|
86
|
+
get packageJSONLocations () {
|
|
87
|
+
return [
|
|
88
|
+
`${this.sourcePath}bower.json`
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** @returns {string} */
|
|
93
|
+
get name () {
|
|
94
|
+
return this.packageJSONFile.firstFileItem.item.name
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** @returns {string} */
|
|
98
|
+
get version () {
|
|
99
|
+
return this.packageJSONFile.firstFileItem.item.version
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** @returns {string} */
|
|
103
|
+
get requiredFramework () {
|
|
104
|
+
return this.packageJSONFile.firstFileItem.item.framework
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** @returns {boolean} */
|
|
108
|
+
get isFrameworkCompatible () {
|
|
109
|
+
if (!this.framework || !this.framework.version) return true
|
|
110
|
+
|
|
111
|
+
return semver.satisfies(this.framework.version, this.requiredFramework)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Returns the plugin type name.
|
|
116
|
+
* @returns {string}
|
|
117
|
+
*/
|
|
118
|
+
get type () {
|
|
119
|
+
if (this.name === 'core') {
|
|
120
|
+
return 'component'
|
|
121
|
+
}
|
|
122
|
+
const config = this.packageJSONFile.firstFileItem.item
|
|
123
|
+
const configKeys = Object.keys(config)
|
|
124
|
+
const typeKeyName = ['component', 'extension', 'menu', 'theme']
|
|
125
|
+
const foundType = configKeys.find(key => typeKeyName.includes(key))
|
|
126
|
+
if (!foundType) {
|
|
127
|
+
throw new Error(`Unknown plugin type for ${this.name}`)
|
|
128
|
+
}
|
|
129
|
+
return foundType
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @returns {string}
|
|
134
|
+
*/
|
|
135
|
+
get targetAttribute () {
|
|
136
|
+
if (this.type === 'component') {
|
|
137
|
+
return this.packageJSONFile.firstFileItem.item.component
|
|
138
|
+
}
|
|
139
|
+
return this.packageJSONFile.firstFileItem.item.targetAttribute
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export default Plugin
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import GlobalsSchema from './GlobalsSchema.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Represents a model extension schema. Currently these schemas are only able to
|
|
5
|
+
* extend config, course, contentobject, article, block and component models.
|
|
6
|
+
*
|
|
7
|
+
* @todo pluginLocations is not a good way of listing model extensions or detecting
|
|
8
|
+
* extension schemas. There should be a schema type (model/extension) and
|
|
9
|
+
* the extensions should be declared at the root of the schema.
|
|
10
|
+
* @todo pluginLocations[modelName] should extend any model (article, block,
|
|
11
|
+
* accordion, narrative)
|
|
12
|
+
*/
|
|
13
|
+
class ExtensionSchema extends GlobalsSchema {
|
|
14
|
+
/**
|
|
15
|
+
* Returns the defined model extension sub-schemas listed at pluginLocations.
|
|
16
|
+
* @returns {Object|undefined}
|
|
17
|
+
*/
|
|
18
|
+
getModelExtensionParts () {
|
|
19
|
+
if (!this.json.properties.pluginLocations || !this.json.properties.pluginLocations.properties) {
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
return this.json.properties.pluginLocations && this.json.properties.pluginLocations.properties
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default ExtensionSchema
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import Schema from './Schema.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import('./Framework')} Framework
|
|
5
|
+
* @typedef {import('../plugins/Plugin')} Plugin
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Represents the globals properties listed in a model or extension schema.
|
|
10
|
+
* @todo _globals doesn't need to differentiate by plugin type, plugin name should suffice
|
|
11
|
+
* @todo We should drop all pluralisations, they're unnecessary and complicated.
|
|
12
|
+
*/
|
|
13
|
+
class GlobalsSchema extends Schema {
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object} options
|
|
16
|
+
* @param {Framework} options.framework
|
|
17
|
+
* @param {string} options.name
|
|
18
|
+
* @param {Plugin} options.plugin
|
|
19
|
+
* @param {Object} options.json
|
|
20
|
+
* @param {string} options.filePath
|
|
21
|
+
* @param {string} options.globalsType
|
|
22
|
+
* @param {string} options.targetAttribute Attribute where this sub-schema will be injected into the course.json:_globals._[pluginType] object
|
|
23
|
+
*/
|
|
24
|
+
constructor ({
|
|
25
|
+
framework = null,
|
|
26
|
+
name = '',
|
|
27
|
+
plugin = null,
|
|
28
|
+
json = {},
|
|
29
|
+
filePath = '',
|
|
30
|
+
globalsType = '',
|
|
31
|
+
targetAttribute = ''
|
|
32
|
+
} = {}) {
|
|
33
|
+
super({ framework, name, plugin, json, filePath, globalsType })
|
|
34
|
+
// Add an underscore to the front of the targetAttribute if necessary
|
|
35
|
+
this.targetAttribute = (targetAttribute && targetAttribute[0] !== '_' ? '_' : '') + targetAttribute
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Returns the sub-schema for the course.json:_globals object.
|
|
40
|
+
* @returns {Object|undefined}
|
|
41
|
+
*/
|
|
42
|
+
getCourseGlobalsPart () {
|
|
43
|
+
if (!this.json.globals) {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* pluralise location name if necessary (components, extensions) etc
|
|
48
|
+
*/
|
|
49
|
+
const shouldPluralise = ['component', 'extension'].includes(this.globalsType)
|
|
50
|
+
const globalsType = shouldPluralise ? this.globalsType + 's' : this.globalsType
|
|
51
|
+
return {
|
|
52
|
+
[`_${globalsType}`]: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: {
|
|
55
|
+
[this.targetAttribute]: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: this.json.globals
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default GlobalsSchema
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import GlobalsSchema from './GlobalsSchema.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Represents an article, block, accordion or other type of model schema
|
|
5
|
+
*/
|
|
6
|
+
class ModelSchema extends GlobalsSchema {
|
|
7
|
+
/**
|
|
8
|
+
* Create array of translatable attribute paths
|
|
9
|
+
* @returns {[string]}
|
|
10
|
+
*/
|
|
11
|
+
getTranslatablePaths () {
|
|
12
|
+
const paths = {}
|
|
13
|
+
this.traverse('', ({ description, next }, attributePath) => {
|
|
14
|
+
switch (description.type) {
|
|
15
|
+
case 'object':
|
|
16
|
+
next(attributePath + description.name + '/')
|
|
17
|
+
break
|
|
18
|
+
case 'array':
|
|
19
|
+
if (!Object.prototype.hasOwnProperty.call(description, 'items')) {
|
|
20
|
+
// handles 'inputType': 'List' edge-case
|
|
21
|
+
break
|
|
22
|
+
}
|
|
23
|
+
if (description.items.type === 'object') {
|
|
24
|
+
next(attributePath + description.name + '/')
|
|
25
|
+
} else {
|
|
26
|
+
next(attributePath)
|
|
27
|
+
}
|
|
28
|
+
break
|
|
29
|
+
case 'string': {
|
|
30
|
+
// check if attribute should be picked
|
|
31
|
+
const value = Boolean(description.translatable)
|
|
32
|
+
if (value === false) {
|
|
33
|
+
break
|
|
34
|
+
}
|
|
35
|
+
// add value to store
|
|
36
|
+
paths[attributePath + description.name + '/'] = value
|
|
37
|
+
break
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}, '/')
|
|
41
|
+
return Object.keys(paths)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default ModelSchema
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import('./Framework')} Framework
|
|
5
|
+
* @typedef {import('./ModelSchema')} ModelSchema
|
|
6
|
+
* @typedef {import('./Schema')} Schema
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Encapsulates a collection of ModelSchema
|
|
11
|
+
* @todo Validation, maybe, if this set of files is used in the AAT
|
|
12
|
+
*/
|
|
13
|
+
class ModelSchemas {
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object} options
|
|
16
|
+
* @param {Framework} options.framework
|
|
17
|
+
* @param {[ModelSchema]} options.schemas
|
|
18
|
+
*/
|
|
19
|
+
constructor ({
|
|
20
|
+
framework = null,
|
|
21
|
+
schemas = []
|
|
22
|
+
} = {}) {
|
|
23
|
+
/** @type {Framework} */
|
|
24
|
+
this.framework = framework
|
|
25
|
+
/** @type {[ModelSchema]} */
|
|
26
|
+
this.schemas = schemas
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Returns an array of translatable attribute paths derived from the schemas.
|
|
31
|
+
* @returns {[string]}
|
|
32
|
+
*/
|
|
33
|
+
getTranslatablePaths () {
|
|
34
|
+
return _.uniq(this.schemas.reduce((paths, modelSchema) => {
|
|
35
|
+
paths.push(...modelSchema.getTranslatablePaths())
|
|
36
|
+
return paths
|
|
37
|
+
}, []))
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default ModelSchemas
|