coh-content-db 2.0.0-rc.8 → 2.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/build.yml +3 -1
- package/CHANGELOG.md +47 -0
- package/README.md +48 -17
- package/dist/coh-content-db.d.ts +257 -200
- package/dist/coh-content-db.js +509 -339
- package/dist/coh-content-db.js.map +1 -1
- package/dist/coh-content-db.mjs +501 -335
- package/dist/coh-content-db.mjs.map +1 -1
- package/jest.config.mjs +1 -0
- package/package.json +14 -14
- package/src/main/api/badge-data.ts +13 -7
- package/src/main/api/{content-bundle.ts → bundle-data.ts} +5 -27
- package/src/main/api/bundle-header-data.ts +44 -0
- package/src/main/api/contact-data.ts +2 -1
- package/src/main/api/level-range-data.ts +4 -0
- package/src/main/api/mission-data.ts +3 -29
- package/src/main/api/mission-flashback-data.ts +31 -0
- package/src/main/api/morality.ts +27 -9
- package/src/main/api/set-title-data.ts +4 -0
- package/src/main/api/variant-context.ts +11 -0
- package/src/main/api/{alternate-data.ts → variant-data.ts} +4 -4
- package/src/main/api/zone-data.ts +24 -0
- package/src/main/api/zone-type.ts +59 -0
- package/src/main/db/abstract-index.ts +12 -16
- package/src/main/db/badge-index.ts +53 -27
- package/src/main/db/badge-requirement.ts +1 -1
- package/src/main/db/badge-search-options.ts +15 -14
- package/src/main/db/badge.ts +46 -29
- package/src/main/db/bundle-header.ts +52 -0
- package/src/main/db/coh-content-database.ts +22 -22
- package/src/main/db/contact.ts +5 -4
- package/src/main/db/level-range.ts +15 -0
- package/src/main/db/mission.ts +11 -10
- package/src/main/db/paged.ts +7 -3
- package/src/main/db/set-title-ids.ts +10 -0
- package/src/main/db/variants.ts +84 -0
- package/src/main/db/zone.ts +29 -0
- package/src/main/index.ts +14 -8
- package/src/main/util/coalesce-to-array.ts +13 -0
- package/src/main/{util.ts → util/links.ts} +8 -22
- package/src/main/util/to-date.ts +9 -0
- package/src/test/api/alignment.test.ts +2 -2
- package/src/test/api/badge-data.fixture.ts +1 -0
- package/src/test/api/badge-data.test.ts +1 -0
- package/src/test/api/bundle-data.fixture.ts +7 -0
- package/src/test/api/bundle-header-data.fixture.ts +8 -0
- package/src/test/api/morality.test.ts +31 -0
- package/src/test/api/sex.test.ts +2 -2
- package/src/test/api/zone-data.fixture.ts +1 -0
- package/src/test/db/abstract-index.test.ts +12 -43
- package/src/test/db/badge-index.test.ts +197 -101
- package/src/test/db/badge.test.ts +122 -16
- package/src/test/db/bundle-header.test.ts +89 -0
- package/src/test/db/coh-content-database.test.ts +137 -178
- package/src/test/db/contact.test.ts +2 -1
- package/src/test/db/level-range.test.ts +47 -0
- package/src/test/db/mission.test.ts +8 -6
- package/src/test/db/morality-list.test.ts +1 -1
- package/src/test/db/set-title-ids.test.ts +19 -0
- package/src/test/db/{alternates.test.ts → variants.test.ts} +24 -24
- package/src/test/db/zone.test.ts +45 -0
- package/src/test/integration.test.ts +16 -0
- package/src/test/util/coalese-to-array.test.ts +17 -0
- package/src/test/{util.test.ts → util/links.test.ts} +5 -21
- package/src/test/util/to-date.test.ts +15 -0
- package/src/main/api/change.ts +0 -17
- package/src/main/changelog.ts +0 -29
- package/src/main/db/alternates.ts +0 -67
- package/src/main/db/bundle-metadata.ts +0 -45
- package/src/test/api/content-bundle.fixture.ts +0 -6
- package/src/test/api/content-bundle.test.ts +0 -14
- package/src/test/changelog.test.ts +0 -36
- package/src/test/db/bundle-metadata.test.ts +0 -84
- package/src/test/index.test.ts +0 -14
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Variants } from '../../main'
|
|
2
2
|
|
|
3
|
-
describe(
|
|
3
|
+
describe(Variants.name, () => {
|
|
4
4
|
describe('Constructor', () => {
|
|
5
|
-
test('should accept a list of
|
|
6
|
-
new
|
|
5
|
+
test('should accept a list of variant values', () => {
|
|
6
|
+
new Variants([
|
|
7
7
|
{ value: 'Default' },
|
|
8
8
|
{ sex: 'M', value: 'Male' },
|
|
9
9
|
{ alignment: 'hero', value: 'Hero' },
|
|
@@ -13,17 +13,17 @@ describe(Alternates.name, () => {
|
|
|
13
13
|
})
|
|
14
14
|
|
|
15
15
|
test('should accept a single value', () => {
|
|
16
|
-
expect(new
|
|
16
|
+
expect(new Variants('foo').default?.value).toBe('foo')
|
|
17
17
|
})
|
|
18
18
|
})
|
|
19
19
|
|
|
20
20
|
describe('getValue', () => {
|
|
21
21
|
test('should return undefined if there are no values', () => {
|
|
22
|
-
expect(new
|
|
22
|
+
expect(new Variants([]).getValue()).toBeUndefined()
|
|
23
23
|
})
|
|
24
24
|
|
|
25
25
|
test('should return the least-specific value when no classifiers are provided', () => {
|
|
26
|
-
expect(new
|
|
26
|
+
expect(new Variants([
|
|
27
27
|
{ value: 'Default' },
|
|
28
28
|
{ sex: 'M', value: 'Male' },
|
|
29
29
|
{ alignment: 'hero', value: 'Hero' },
|
|
@@ -33,7 +33,7 @@ describe(Alternates.name, () => {
|
|
|
33
33
|
})
|
|
34
34
|
|
|
35
35
|
test('should return the least-specific value when no classifiers are provided, regardless of insert order', () => {
|
|
36
|
-
expect(new
|
|
36
|
+
expect(new Variants([
|
|
37
37
|
{ alignment: 'villain', sex: 'M', value: 'Male Villain' },
|
|
38
38
|
{ alignment: 'hero', value: 'Hero' },
|
|
39
39
|
{ value: 'Default' },
|
|
@@ -43,27 +43,27 @@ describe(Alternates.name, () => {
|
|
|
43
43
|
})
|
|
44
44
|
|
|
45
45
|
test('should return the most specific match', () => {
|
|
46
|
-
expect(new
|
|
46
|
+
expect(new Variants([
|
|
47
47
|
{ value: 'Default' },
|
|
48
48
|
{ sex: 'M', value: 'Male' },
|
|
49
49
|
{ alignment: 'hero', value: 'Hero' },
|
|
50
50
|
{ alignment: 'villain', sex: 'M', value: 'Male Villain' },
|
|
51
51
|
{ alignment: 'praetorian', sex: 'F', value: 'Praetorian Female' },
|
|
52
|
-
]).getValue('villain', 'M')).toBe('Male Villain')
|
|
52
|
+
]).getValue({ morality: 'villain', sex: 'M' })).toBe('Male Villain')
|
|
53
53
|
})
|
|
54
54
|
|
|
55
55
|
test('should return the most specific match, regardless of insert order', () => {
|
|
56
|
-
expect(new
|
|
56
|
+
expect(new Variants([
|
|
57
57
|
{ alignment: 'praetorian', sex: 'F', value: 'Praetorian Female' },
|
|
58
58
|
{ sex: 'M', value: 'Male' },
|
|
59
59
|
{ alignment: 'hero', value: 'Hero' },
|
|
60
60
|
{ alignment: 'villain', sex: 'M', value: 'Male Villain' },
|
|
61
61
|
{ value: 'Default' },
|
|
62
|
-
]).getValue('villain', 'M')).toBe('Male Villain')
|
|
62
|
+
]).getValue({ morality: 'villain', sex: 'M' })).toBe('Male Villain')
|
|
63
63
|
})
|
|
64
64
|
|
|
65
65
|
test('should return the lowest canonical value if there is no default', () => {
|
|
66
|
-
expect(new
|
|
66
|
+
expect(new Variants([
|
|
67
67
|
{ alignment: 'hero', value: 'Hero' },
|
|
68
68
|
{ sex: 'M', value: 'Male' },
|
|
69
69
|
{ alignment: 'praetorian', sex: 'F', value: 'Praetorian Female' },
|
|
@@ -72,22 +72,22 @@ describe(Alternates.name, () => {
|
|
|
72
72
|
})
|
|
73
73
|
|
|
74
74
|
test('should return the lowest canonical value if a specific is requested that does not exist', () => {
|
|
75
|
-
expect(new
|
|
75
|
+
expect(new Variants([
|
|
76
76
|
{ alignment: 'hero', value: 'Hero' },
|
|
77
77
|
{ alignment: 'villain', value: 'Villain' },
|
|
78
78
|
{ alignment: 'villain', sex: 'M', value: 'Male Villain' },
|
|
79
79
|
{ alignment: 'praetorian', sex: 'F', value: 'Praetorian Female' },
|
|
80
|
-
]).getValue(
|
|
80
|
+
]).getValue({ sex: 'F' })).toBe('Hero')
|
|
81
81
|
})
|
|
82
82
|
})
|
|
83
83
|
|
|
84
84
|
describe('default', () => {
|
|
85
85
|
test('should return undefined if there are no values', () => {
|
|
86
|
-
expect(new
|
|
86
|
+
expect(new Variants([]).default).toBeUndefined()
|
|
87
87
|
})
|
|
88
88
|
|
|
89
89
|
test('should return the lowest priority value', () => {
|
|
90
|
-
expect(new
|
|
90
|
+
expect(new Variants([
|
|
91
91
|
{ value: 'Default' },
|
|
92
92
|
{ sex: 'M', value: 'Male' },
|
|
93
93
|
{ alignment: 'hero', value: 'Hero' },
|
|
@@ -95,7 +95,7 @@ describe(Alternates.name, () => {
|
|
|
95
95
|
{ alignment: 'praetorian', sex: 'F', value: 'Praetorian Female' },
|
|
96
96
|
]).default?.value).toBe('Default')
|
|
97
97
|
|
|
98
|
-
expect(new
|
|
98
|
+
expect(new Variants([
|
|
99
99
|
{ alignment: 'villain', sex: 'M', value: 'Male Villain' },
|
|
100
100
|
{ alignment: 'praetorian', sex: 'F', value: 'Praetorian Female' },
|
|
101
101
|
{ sex: 'M', value: 'Male' },
|
|
@@ -103,7 +103,7 @@ describe(Alternates.name, () => {
|
|
|
103
103
|
{ alignment: 'hero', value: 'Hero' },
|
|
104
104
|
]).default?.value).toBe('Default')
|
|
105
105
|
|
|
106
|
-
expect(new
|
|
106
|
+
expect(new Variants([
|
|
107
107
|
{ alignment: 'villain', sex: 'M', value: 'Male Villain' },
|
|
108
108
|
{ alignment: 'praetorian', sex: 'F', value: 'Praetorian Female' },
|
|
109
109
|
{ sex: 'M', value: 'Male' },
|
|
@@ -114,11 +114,11 @@ describe(Alternates.name, () => {
|
|
|
114
114
|
|
|
115
115
|
describe('canonical', () => {
|
|
116
116
|
test('should be empty if there are no values', () => {
|
|
117
|
-
expect(new
|
|
117
|
+
expect(new Variants([]).canonical).toHaveLength(0)
|
|
118
118
|
})
|
|
119
119
|
|
|
120
120
|
test('should return values sorted in canonical order', () => {
|
|
121
|
-
const result = new
|
|
121
|
+
const result = new Variants([
|
|
122
122
|
{ alignment: 'hero', sex: 'F', value: 'Female Hero' },
|
|
123
123
|
{ alignment: 'praetorian', value: 'Praetorian' },
|
|
124
124
|
{ sex: 'F', value: 'Female' },
|
|
@@ -150,7 +150,7 @@ describe(Alternates.name, () => {
|
|
|
150
150
|
})
|
|
151
151
|
|
|
152
152
|
test('should sort unspecified values by alpha', () => {
|
|
153
|
-
expect(new
|
|
153
|
+
expect(new Variants([
|
|
154
154
|
{ value: 'A' },
|
|
155
155
|
{ value: 'C' },
|
|
156
156
|
{ value: 'B' },
|
|
@@ -162,7 +162,7 @@ describe(Alternates.name, () => {
|
|
|
162
162
|
})
|
|
163
163
|
|
|
164
164
|
test('should sort identical values by value alpha', () => {
|
|
165
|
-
expect(new
|
|
165
|
+
expect(new Variants([
|
|
166
166
|
{ alignment: 'villain', value: 'B' },
|
|
167
167
|
{ sex: 'M', value: 'D' },
|
|
168
168
|
{ alignment: 'villain', value: 'A' },
|
|
@@ -178,7 +178,7 @@ describe(Alternates.name, () => {
|
|
|
178
178
|
|
|
179
179
|
describe('toString', () => {
|
|
180
180
|
test('should create a string separated by the separator', () => {
|
|
181
|
-
expect(new
|
|
181
|
+
expect(new Variants([
|
|
182
182
|
{ sex: 'M', value: 'A' },
|
|
183
183
|
{ sex: 'F', value: 'B' },
|
|
184
184
|
{ alignment: 'hero', value: 'C' },
|
package/src/test/db/zone.test.ts
CHANGED
|
@@ -22,6 +22,51 @@ describe(Zone.name, () => {
|
|
|
22
22
|
})
|
|
23
23
|
})
|
|
24
24
|
|
|
25
|
+
describe('type', () => {
|
|
26
|
+
test(`should be set from the data`, () => {
|
|
27
|
+
const zone = new Zone(zoneDataFixture.create({ type: 'city' }))
|
|
28
|
+
expect(zone.type).toEqual('city')
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('morality', () => {
|
|
33
|
+
test(`should be set from the data`, () => {
|
|
34
|
+
const zone = new Zone(zoneDataFixture.create({ morality: ['hero'] }))
|
|
35
|
+
expect(zone.morality?.hero).toBeTruthy()
|
|
36
|
+
expect(zone.morality?.vigilante).toBeFalsy()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test(`should be optional`, () => {
|
|
40
|
+
const zone = new Zone(zoneDataFixture.omit('morality').create())
|
|
41
|
+
expect(zone.morality?.all).toBeTruthy()
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('levelRange', () => {
|
|
46
|
+
test(`should be set from the data`, () => {
|
|
47
|
+
const zone = new Zone(zoneDataFixture.create({ levelRange: [10] }))
|
|
48
|
+
expect(zone.levelRange?.min).toEqual(10)
|
|
49
|
+
expect(zone.levelRange?.max).toBeUndefined()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test(`should be optional`, () => {
|
|
53
|
+
const zone = new Zone(zoneDataFixture.omit('levelRange').create())
|
|
54
|
+
expect(zone.levelRange).toBeUndefined()
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('notes', () => {
|
|
59
|
+
test(`should be set from the data`, () => {
|
|
60
|
+
const zone = new Zone(zoneDataFixture.create({ notes: 'foo' }))
|
|
61
|
+
expect(zone.notes).toEqual('foo')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test(`should be optional`, () => {
|
|
65
|
+
const zone = new Zone(zoneDataFixture.omit('notes').create())
|
|
66
|
+
expect(zone.notes).toBeUndefined()
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
25
70
|
describe('links', () => {
|
|
26
71
|
test(`should be set from the data`, () => {
|
|
27
72
|
const zone = new Zone(zoneDataFixture.create({ links: [{ title: 'foo', href: 'bar' }] }))
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BundleData } from '../main'
|
|
2
|
+
import { TEST_BADGE } from './api/badge-data.test'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* If you change this test, update the example in the README as well
|
|
6
|
+
*/
|
|
7
|
+
export const MyBundle: BundleData = {
|
|
8
|
+
header: { name: 'My Content Bundle', version: '1.0.0', lastUpdateTime: '2025-04-21T00:00:00Z' },
|
|
9
|
+
badges: [TEST_BADGE],
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('BundleData', () => {
|
|
13
|
+
test('should be a usable interface', () => {
|
|
14
|
+
expect(MyBundle).not.toBeUndefined()
|
|
15
|
+
})
|
|
16
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { coalesceToArray } from '../../main/util/coalesce-to-array'
|
|
2
|
+
|
|
3
|
+
describe(coalesceToArray.name, () => {
|
|
4
|
+
test('should return an array unmodified', () => {
|
|
5
|
+
expect(coalesceToArray(['a', 'b'])).toStrictEqual(['a', 'b'])
|
|
6
|
+
expect(coalesceToArray([1, 2])).toStrictEqual([1, 2])
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
test('should return a single value as a single-value array', () => {
|
|
10
|
+
expect(coalesceToArray('a')).toStrictEqual(['a'])
|
|
11
|
+
expect(coalesceToArray(1)).toStrictEqual([1])
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('should return undefined value as undefined', () => {
|
|
15
|
+
expect(coalesceToArray()).toBeUndefined()
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { Badge, badgeLink, badgeUri,
|
|
2
|
-
import { badgeDataFixture } from '
|
|
3
|
-
import { zoneDataFixture } from '
|
|
4
|
-
import { contactDataFixture } from '
|
|
5
|
-
import { missionDataFixture } from '
|
|
1
|
+
import { Badge, badgeLink, badgeUri, Contact, contactLink, contactUri, Mission, missionLink, missionUri, Zone, zoneLink, zoneUri } from '../../main'
|
|
2
|
+
import { badgeDataFixture } from '../api/badge-data.fixture'
|
|
3
|
+
import { zoneDataFixture } from '../api/zone-data.fixture'
|
|
4
|
+
import { contactDataFixture } from '../api/contact-data.fixture'
|
|
5
|
+
import { missionDataFixture } from '../api/mission-data.fixture'
|
|
6
6
|
|
|
7
7
|
describe(badgeUri.name, () => {
|
|
8
8
|
test('should return the expected pattern', () => {
|
|
@@ -147,19 +147,3 @@ describe(zoneLink.name, () => {
|
|
|
147
147
|
expect(zoneLink(zone)).toBe('[foo](zone://foo)')
|
|
148
148
|
})
|
|
149
149
|
})
|
|
150
|
-
|
|
151
|
-
describe(coalesceToArray.name, () => {
|
|
152
|
-
test('should return an array unmodified', () => {
|
|
153
|
-
expect(coalesceToArray(['a', 'b'])).toStrictEqual(['a', 'b'])
|
|
154
|
-
expect(coalesceToArray([1, 2])).toStrictEqual([1, 2])
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
test('should return a single value as a single-value array', () => {
|
|
158
|
-
expect(coalesceToArray('a')).toStrictEqual(['a'])
|
|
159
|
-
expect(coalesceToArray(1)).toStrictEqual([1])
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
test('should return undefined value as undefined', () => {
|
|
163
|
-
expect(coalesceToArray()).toBeUndefined()
|
|
164
|
-
})
|
|
165
|
-
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { toDate } from '../../main/util/to-date'
|
|
2
|
+
|
|
3
|
+
describe(toDate.name, () => {
|
|
4
|
+
test('should return a valid date', () => {
|
|
5
|
+
expect(toDate('2025-02-01')).toStrictEqual(new Date('2025-02-01'))
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
test('should return a valid time', () => {
|
|
9
|
+
expect(toDate('2025-04-21T02:57:52.402Z')).toStrictEqual(new Date('2025-04-21T02:57:52.402Z'))
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('should throw on invalid ISO string', () => {
|
|
13
|
+
expect(() => toDate('foo')).toThrow('Invalid date')
|
|
14
|
+
})
|
|
15
|
+
})
|
package/src/main/api/change.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { MarkdownString } from './markdown-string'
|
|
2
|
-
|
|
3
|
-
export interface Change {
|
|
4
|
-
/**
|
|
5
|
-
* The version number in {@link http://semver.org|semver} format.
|
|
6
|
-
*/
|
|
7
|
-
version: string
|
|
8
|
-
/**
|
|
9
|
-
* Date of the change.
|
|
10
|
-
*/
|
|
11
|
-
date: Date
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Description of the change.
|
|
15
|
-
*/
|
|
16
|
-
description: MarkdownString
|
|
17
|
-
}
|
package/src/main/changelog.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { Change } from './api/change'
|
|
2
|
-
|
|
3
|
-
export const CHANGELOG: Change[] = [
|
|
4
|
-
{
|
|
5
|
-
version: '2.0.0',
|
|
6
|
-
date: new Date('2025-03-12'),
|
|
7
|
-
description: ''
|
|
8
|
-
+ '* Replaced redundant interfaces with their concrete equivalents.\n'
|
|
9
|
-
+ `* Server groups are now referred to as 'forks'.\n`
|
|
10
|
-
+ '* Replaced enums with union types and changed values to `kebab-case`.\n'
|
|
11
|
-
+ '* `IServerGroupData` is now `ContentBundle` and each database instance is now designed to accept only a single bundle.\n'
|
|
12
|
-
+ '* `GameMap` is now `Zone`.\n'
|
|
13
|
-
+ '* Removed the `serverGroup` property from entities to simplify the object tree given that only a single context can exist per db now.\n'
|
|
14
|
-
+ '* Added a simple indexing and search function for badge names, text and acquisition info.\n'
|
|
15
|
-
+ '* Zone and badge references now follow a standard Markdown link format with a `badge://` or `map://` protocol.\n'
|
|
16
|
-
+ '* Badge partials are now known as badge requirements.\n'
|
|
17
|
-
+ '* Removed the `VidiotMap` API as it was never used or fleshed out properly.\n'
|
|
18
|
-
+ '* Added formal support for Missions and Contacts in badge requirements.\n'
|
|
19
|
-
+ '* Move exploration badge locations into badge requirement list.\n'
|
|
20
|
-
+ '* Standardized pluralization of some field names (name, icon).\n'
|
|
21
|
-
+ '* Combined `settitle` ids into a single tuple field.\n'
|
|
22
|
-
+ '* Change from GNU to The Unlicense.\n'
|
|
23
|
-
+ '* Removed all third-party dependencies.\n'
|
|
24
|
-
+ '* Moved from webpack to rollup for packaging.\n'
|
|
25
|
-
+ '* Add eslint for linting.\n'
|
|
26
|
-
+ '* Add jest for unit tests.\n'
|
|
27
|
-
+ '* Added GitHub Actions for CI.\n',
|
|
28
|
-
},
|
|
29
|
-
]
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { AlternateData } from '../api/alternate-data'
|
|
2
|
-
import { compareSex, Sex } from '../api/sex'
|
|
3
|
-
import { Alignment, compareAlignment } from '../api/alignment'
|
|
4
|
-
|
|
5
|
-
export class Alternates<T> {
|
|
6
|
-
readonly #sortedValues: AlternateData<T>[] = []
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Create an alternate set from either a list of categorized values, or a single value when there are no alternates.
|
|
10
|
-
* @param value List of alternates, or a single value.
|
|
11
|
-
*/
|
|
12
|
-
constructor(value: AlternateData<T>[] | T) {
|
|
13
|
-
if (Array.isArray(value)) {
|
|
14
|
-
this.#sortedValues = value.sort()
|
|
15
|
-
this.#sortedValues.sort((a, b) => this.#compareAlternates(a, b))
|
|
16
|
-
} else {
|
|
17
|
-
this.#sortedValues = [{ value }]
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
getValue(alignment?: Alignment, sex?: Sex): T | undefined {
|
|
22
|
-
for (let index = this.#sortedValues.length; index--;) {
|
|
23
|
-
const entry = this.#sortedValues[index]
|
|
24
|
-
if ((entry.alignment === undefined || entry.alignment === alignment)
|
|
25
|
-
&& (entry.sex === undefined || entry.sex === sex)
|
|
26
|
-
) return entry.value
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return this.default?.value
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Get the default value for this list of alternates, the value with the highest priority and lowest specificity.
|
|
34
|
-
*/
|
|
35
|
-
get default(): AlternateData<T> | undefined {
|
|
36
|
-
return this.#sortedValues[0]
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Get the list of alternates sorted in canonical order (alignment then sex, low to high specificity).
|
|
41
|
-
*/
|
|
42
|
-
get canonical(): AlternateData<T>[] {
|
|
43
|
-
return this.#sortedValues
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Create a joined string from the alternate values in canonical order.
|
|
48
|
-
* @param separator Separator to use. Default is ' / '
|
|
49
|
-
*/
|
|
50
|
-
toString(separator: string): string {
|
|
51
|
-
return this.canonical.map(x => x.value).join(separator)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
#compareAlternates(a: AlternateData<T>, b: AlternateData<T>): number {
|
|
55
|
-
const aSpecificity = (a.alignment ? 2 : 0) + (a.sex ? 1 : 0)
|
|
56
|
-
const bSpecificity = (b.alignment ? 2 : 0) + (b.sex ? 1 : 0)
|
|
57
|
-
if (aSpecificity !== bSpecificity) return aSpecificity - bSpecificity // Order first by least-specific
|
|
58
|
-
|
|
59
|
-
const alignmentComparison = compareAlignment(a.alignment, b.alignment) // Next by alignment
|
|
60
|
-
if (alignmentComparison !== 0) return alignmentComparison
|
|
61
|
-
|
|
62
|
-
const sexComparison = compareSex(a.sex, b.sex) // Last by sex
|
|
63
|
-
if (sexComparison !== 0) return sexComparison
|
|
64
|
-
|
|
65
|
-
return String(a.value).localeCompare(String(b.value))
|
|
66
|
-
}
|
|
67
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { ContentBundle } from '../api/content-bundle'
|
|
2
|
-
import { Change } from '../api/change'
|
|
3
|
-
import { Link } from '../api/link'
|
|
4
|
-
import { MarkdownString } from '../api/markdown-string'
|
|
5
|
-
|
|
6
|
-
export class BundleMetadata {
|
|
7
|
-
/**
|
|
8
|
-
* Name of the content bundle.
|
|
9
|
-
*/
|
|
10
|
-
readonly name: string
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Description of the fork.
|
|
14
|
-
*/
|
|
15
|
-
readonly description?: MarkdownString
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Repository where the db content package is maintained.
|
|
19
|
-
*/
|
|
20
|
-
readonly repository?: string
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* List of external links. Wiki, forums, etc.
|
|
24
|
-
*/
|
|
25
|
-
readonly links?: Link[]
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Change log for this data package.
|
|
29
|
-
*/
|
|
30
|
-
readonly changelog?: Change[]
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* The current version of the data package.
|
|
34
|
-
*/
|
|
35
|
-
readonly version?: string
|
|
36
|
-
|
|
37
|
-
constructor(bundle: ContentBundle) {
|
|
38
|
-
this.name = bundle.name
|
|
39
|
-
this.description = bundle.description
|
|
40
|
-
this.repository = bundle.repository
|
|
41
|
-
this.links = bundle.links ?? []
|
|
42
|
-
this.changelog = bundle.changelog ?? []
|
|
43
|
-
this.version = this.changelog?.at(-1)?.version
|
|
44
|
-
}
|
|
45
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { ContentBundle } from '../../main'
|
|
2
|
-
import { TEST_BADGE } from './badge-data.test'
|
|
3
|
-
|
|
4
|
-
// If you change this test, update the example in the README as well
|
|
5
|
-
export const TEST_BUNDLE: ContentBundle = {
|
|
6
|
-
name: 'My Content Bundle',
|
|
7
|
-
badges: [TEST_BADGE],
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
describe('ContentBundle', () => {
|
|
11
|
-
test('should be a usable interface', () => {
|
|
12
|
-
expect(TEST_BUNDLE).not.toBeUndefined()
|
|
13
|
-
})
|
|
14
|
-
})
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { CHANGELOG } from '../main'
|
|
2
|
-
|
|
3
|
-
describe('CHANGELOG', () => {
|
|
4
|
-
test('should be extant', () => {
|
|
5
|
-
expect(CHANGELOG).not.toBeUndefined()
|
|
6
|
-
expect(CHANGELOG).not.toBeUndefined()
|
|
7
|
-
})
|
|
8
|
-
|
|
9
|
-
test('should be an array', () => {
|
|
10
|
-
expect(Array.isArray(CHANGELOG)).toBeTruthy()
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
test('should have only semver versions', () => {
|
|
14
|
-
// semver.org - https://regex101.com/r/vkijKf/1/
|
|
15
|
-
const pattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
|
16
|
-
|
|
17
|
-
for (const change of CHANGELOG) {
|
|
18
|
-
expect(pattern.test(change.version)).toBeTruthy()
|
|
19
|
-
}
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
test('should have dates', () => {
|
|
23
|
-
for (const change of CHANGELOG) {
|
|
24
|
-
const date = change.date
|
|
25
|
-
expect(date).not.toBeUndefined()
|
|
26
|
-
expect(date).not.toBeUndefined()
|
|
27
|
-
}
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
test('should have descriptions', () => {
|
|
31
|
-
for (const change of CHANGELOG) {
|
|
32
|
-
expect(change).not.toBeUndefined()
|
|
33
|
-
expect(change).not.toBeUndefined()
|
|
34
|
-
}
|
|
35
|
-
})
|
|
36
|
-
})
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { BundleMetadata } from '../../main'
|
|
2
|
-
import { contentBundleFixture } from '../api/content-bundle.fixture'
|
|
3
|
-
|
|
4
|
-
describe(BundleMetadata.name, () => {
|
|
5
|
-
describe('Constructor', () => {
|
|
6
|
-
test(`should accept the test fixture`, () => {
|
|
7
|
-
new BundleMetadata(contentBundleFixture.create())
|
|
8
|
-
})
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
describe('name', () => {
|
|
12
|
-
test(`should be read from the bundle`, () => {
|
|
13
|
-
const bundle = new BundleMetadata(contentBundleFixture.create({ name: 'foo' }))
|
|
14
|
-
expect(bundle.name).toBe('foo')
|
|
15
|
-
})
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
describe('description', () => {
|
|
19
|
-
test(`should be read from the bundle`, () => {
|
|
20
|
-
const bundle = new BundleMetadata(contentBundleFixture.create({ description: 'foo' }))
|
|
21
|
-
expect(bundle.description).toBe('foo')
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
test(`should be optional`, () => {
|
|
25
|
-
const bundle = new BundleMetadata(contentBundleFixture.omit('description').create())
|
|
26
|
-
expect(bundle.description).toBeUndefined()
|
|
27
|
-
})
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
describe('repository', () => {
|
|
31
|
-
test(`should be read from the bundle`, () => {
|
|
32
|
-
const bundle = new BundleMetadata(contentBundleFixture.create({ repository: 'foo' }))
|
|
33
|
-
expect(bundle.repository).toBe('foo')
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
test(`should be optional`, () => {
|
|
37
|
-
const bundle = new BundleMetadata(contentBundleFixture.omit('repository').create())
|
|
38
|
-
expect(bundle.repository).toBeUndefined()
|
|
39
|
-
})
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
describe('links', () => {
|
|
43
|
-
test(`should be read from the bundle`, () => {
|
|
44
|
-
const bundle = new BundleMetadata(contentBundleFixture.create({ links: [{ title: 'foo', href: 'bar' }] }))
|
|
45
|
-
expect(bundle.links).toStrictEqual([{ title: 'foo', href: 'bar' }])
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
test(`should be optional`, () => {
|
|
49
|
-
const bundle = new BundleMetadata(contentBundleFixture.omit('links').create())
|
|
50
|
-
expect(bundle.links).toHaveLength(0)
|
|
51
|
-
})
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
describe('changelog', () => {
|
|
55
|
-
test(`should be read from the bundle`, () => {
|
|
56
|
-
const bundle = new BundleMetadata(contentBundleFixture.create({
|
|
57
|
-
changelog: [{ version: 'foo', date: new Date('2025-03-12'), description: 'bar' }],
|
|
58
|
-
}))
|
|
59
|
-
expect(bundle.changelog).toStrictEqual([{ version: 'foo', date: new Date('2025-03-12'), description: 'bar' }])
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
test(`should be optional`, () => {
|
|
63
|
-
const bundle = new BundleMetadata(contentBundleFixture.omit('changelog').create())
|
|
64
|
-
expect(bundle.changelog).toHaveLength(0)
|
|
65
|
-
})
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
describe('version', () => {
|
|
69
|
-
test(`should be read from the latest changelog entry`, () => {
|
|
70
|
-
const bundle = new BundleMetadata(contentBundleFixture.create({
|
|
71
|
-
changelog: [
|
|
72
|
-
{ version: 'foo', date: new Date('2025-03-12'), description: 'Foo' },
|
|
73
|
-
{ version: 'latest', date: new Date('2025-04-12'), description: 'Bar' },
|
|
74
|
-
],
|
|
75
|
-
}))
|
|
76
|
-
expect(bundle.version).toBe('latest')
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
test(`should be undefined if there is no changelog`, () => {
|
|
80
|
-
const bundle = new BundleMetadata(contentBundleFixture.omit('changelog').create())
|
|
81
|
-
expect(bundle.version).toBeUndefined()
|
|
82
|
-
})
|
|
83
|
-
})
|
|
84
|
-
})
|
package/src/test/index.test.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import * as index from '../main/index'
|
|
2
|
-
|
|
3
|
-
test('should export the changelog', () => {
|
|
4
|
-
expect(index).toHaveProperty('CHANGELOG')
|
|
5
|
-
})
|
|
6
|
-
|
|
7
|
-
test('should export badge reference utils', () => {
|
|
8
|
-
expect(index).toHaveProperty('badgeUri')
|
|
9
|
-
expect(index).toHaveProperty('badgeLink')
|
|
10
|
-
expect(index).toHaveProperty('contactUri')
|
|
11
|
-
expect(index).toHaveProperty('contactLink')
|
|
12
|
-
expect(index).toHaveProperty('zoneUri')
|
|
13
|
-
expect(index).toHaveProperty('zoneLink')
|
|
14
|
-
})
|