Skip to content

Commit c6257ca

Browse files
committed
Fixed based on implementation in LyricConverter
1 parent b571c16 commit c6257ca

File tree

14 files changed

+304
-204
lines changed

14 files changed

+304
-204
lines changed

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
{
22
"name": "propresenter-parser",
3-
"version": "1.0.0",
3+
"version": "1.0.1",
44
"description": "Parses ProPresenter 4, 5, and 6 files to extract the data, and can build ProPresenter 5 and 6 files",
5+
"author": {
6+
"name": "Chris Barr",
7+
"url": "http://chrisbarr.me"
8+
},
59
"main": "dist/main/index.js",
610
"typings": "dist/main/index.d.ts",
711
"module": "dist/module/index.js",
@@ -13,7 +17,7 @@
1317
"bugs": {
1418
"url": "https://github.com/FiniteLooper/ProPresenter-Parser/issues"
1519
},
16-
"keywords": ["ProPresenter", "church", "lyrics", "song"],
20+
"keywords": ["ProPresenter", "church", "lyrics", "song", "pro4", "pro5", "pro6"],
1721
"files": [
1822
"LICENSE",
1923
"README.md",

sample-files/v6 - Feature Test.pro6

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="utf-8"?>
2-
<RVPresentationDocument height="1080" width="1920" docType="0" versionNumber="600" usedCount="0" backgroundColor="0.196078431372549 0.643137254901961 0.611764705882353 1" drawingBackgroundColor="true" CCLIDisplay="false" lastDateUsed="2023-06-19T19:11:43+00:00" selectedArrangementID="" category="Song" resourcesDirectory="" notes="" CCLISongTitle="" chordChartPath="" os="1" buildNumber="6016">
2+
<RVPresentationDocument height="1080" width="1920" docType="0" versionNumber="600" usedCount="0" backgroundColor="0.196078431372549 0.643137254901961 0.611764705882353 1" drawingBackgroundColor="true" CCLIDisplay="false" lastDateUsed="2023-06-19T19:11:43+00:00" selectedArrangementID="" category="Song" resourcesDirectory="" notes="" chordChartPath="" os="1" buildNumber="6016">
33
<RVTimeline timeOffset="0" duration="0" selectedMediaTrackIndex="-1" loop="false" rvXMLIvarName="timeline">
44
<array rvXMLIvarName="timeCues" />
55
<array rvXMLIvarName="mediaTracks" />

src/v4/parser.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,36 @@ describe('V4 - Parser', (): void => {
2525
}
2626
});
2727

28+
it('should use empty strings for CCLI properties that do not exist', () => {
29+
let testFile = readFileSync('./sample-files/v4 - Be Near.pro4').toString();
30+
testFile = testFile.replace(/(CCLI(ArtistCredits)|(CopyrightInfo)|(LicenseNumber)|(Publisher)|(SongTitle))=".*?"/g, '');
31+
const parsedSong = parser.parse(testFile);
32+
33+
expect(parsedSong.properties).toEqual({
34+
CCLIArtistCredits: '',
35+
CCLICopyrightInfo: '',
36+
CCLIDisplay: false,
37+
CCLILicenseNumber: '',
38+
CCLIPublisher: '',
39+
CCLISongTitle: '',
40+
album: '',
41+
artist: '',
42+
author: '',
43+
backgroundColor: { r: 0, g: 0, b: 0 },
44+
category: 'Song',
45+
creatorCode: 1349676880,
46+
docType: 0,
47+
drawingBackgroundColor: false,
48+
height: 768,
49+
lastDateUsed: new Date('2010-11-07T00:37:36'),
50+
notes: '',
51+
resourcesDirectory: '',
52+
usedCount: 0,
53+
versionNumber: 400,
54+
width: 1024,
55+
} as IPro4Properties);
56+
});
57+
2858
it('should get the data from "Be Near.pro4"', () => {
2959
const testFile = readFileSync('./sample-files/v4 - Be Near.pro4').toString();
3060
const parsedSong = parser.parse(testFile);

src/v4/parser.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@ export class v4Parser {
4242

4343
private getProperties(doc: IXmlPro4Doc): IPro4Properties {
4444
return {
45-
CCLIArtistCredits: doc['@CCLIArtistCredits'],
46-
CCLICopyrightInfo: doc['@CCLICopyrightInfo'],
45+
CCLIArtistCredits: doc['@CCLIArtistCredits'] ?? '',
46+
CCLICopyrightInfo: doc['@CCLICopyrightInfo'] ?? '',
4747
CCLIDisplay: Boolean(doc['@CCLIDisplay']),
48-
CCLILicenseNumber: doc['@CCLILicenseNumber'],
49-
CCLIPublisher: doc['@CCLIPublisher'],
50-
CCLISongTitle: doc['@CCLISongTitle'],
48+
CCLILicenseNumber: doc['@CCLILicenseNumber'] ?? '',
49+
CCLIPublisher: doc['@CCLIPublisher'] ?? '',
50+
CCLISongTitle: doc['@CCLISongTitle'] ?? '',
5151
album: doc['@album'],
5252
artist: doc['@artist'],
5353
author: doc['@author'],

src/v4/xml.model.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ export interface IXmlPro4DocRoot {
44
}
55

66
export interface IXmlPro4Doc {
7-
'@CCLIArtistCredits': string;
8-
'@CCLICopyrightInfo': number;
7+
'@CCLIArtistCredits'?: string;
8+
'@CCLICopyrightInfo'?: number;
99
'@CCLIDisplay': number;
10-
'@CCLILicenseNumber': string | number;
11-
'@CCLIPublisher': string;
12-
'@CCLISongTitle': string;
10+
'@CCLILicenseNumber'?: string | number;
11+
'@CCLIPublisher'?: string;
12+
'@CCLISongTitle'?: string;
1313
'@album': string;
1414
'@artist': string;
1515
'@author': string;

src/v5/builder.spec.ts

Lines changed: 150 additions & 150 deletions
Large diffs are not rendered by default.

src/v5/builder.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ export class v5Builder {
3030
format: true,
3131
ignoreAttributes: false,
3232
processEntities: false,
33+
suppressUnpairedNode: false,
34+
unpairedTags: [
35+
'arrangements',
36+
'timeCues',
37+
'mediaTracks',
38+
'bibleReference',
39+
'cues',
40+
'_-RVProTransitionObject-_transitionObject',
41+
'_-RVRect3D-_position',
42+
'NSColor',
43+
'NSNumber',
44+
'NSMutableString',
45+
],
3346
});
3447

3548
//Set the options, and force the type
@@ -226,6 +239,7 @@ export class v5Builder {
226239
'@revealType': 0,
227240
'@serialization-array-index': 0,
228241
stroke: {
242+
'@containerClass': 'NSMutableDictionary',
229243
NSColor: {
230244
'@serialization-native-value': '0 0 0 1',
231245
'@serialization-dictionary-key': 'RVShapeElementStrokeColorKey',
@@ -236,6 +250,7 @@ export class v5Builder {
236250
},
237251
},
238252
'_-D-_serializedShadow': {
253+
'@containerClass': 'NSMutableDictionary',
239254
NSMutableString: {
240255
'@serialization-native-value': `{3.4641016, -2}`,
241256
'@serialization-dictionary-key': 'shadowOffset',

src/v5/parser.spec.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,53 @@ describe('V5 - Parser', (): void => {
2525
}
2626
});
2727

28+
it('should use empty strings for CCLI properties that do not exist', () => {
29+
let testFile = readFileSync('./sample-files/v5 - Be Near.pro5').toString();
30+
testFile = testFile.replace(/(CCLI(ArtistCredits)|(CopyrightInfo)|(LicenseNumber)|(Publisher)|(SongTitle))=".*?"/g, '');
31+
const parsedSong = parser.parse(testFile);
32+
33+
expect(parsedSong.properties).toEqual({
34+
CCLIArtistCredits: '',
35+
CCLICopyrightInfo: '',
36+
CCLIDisplay: false,
37+
CCLILicenseNumber: '',
38+
CCLIPublisher: '',
39+
CCLISongTitle: '',
40+
album: '',
41+
artist: 'Shane Bernard',
42+
author: '',
43+
backgroundColor: { r: 0, g: 0, b: 0 },
44+
category: 'Song',
45+
creatorCode: 1349676880,
46+
chordChartPath: '',
47+
docType: 0,
48+
drawingBackgroundColor: false,
49+
height: 1050,
50+
lastDateUsed: new Date('2014-10-12T20:44:32'),
51+
notes: '',
52+
resourcesDirectory: '',
53+
usedCount: 0,
54+
versionNumber: 500,
55+
width: 1680,
56+
} as IPro5Properties);
57+
});
58+
59+
it('should not return arrangements for a song with no arrangements specified', () => {
60+
let testFile = readFileSync('./sample-files/v5 - Be Near.pro5').toString();
61+
testFile = testFile.replace(/<arrangements[\s\S]+?<\/arrangements>/g, '');
62+
const parsedSong = parser.parse(testFile);
63+
64+
expect(parsedSong.arrangements).toEqual([]);
65+
});
66+
67+
it('use an empty string when a group label is missing', () => {
68+
let testFile = readFileSync('./sample-files/v5 - Be Near.pro5').toString();
69+
testFile = testFile.replace('name="Background" ', '');
70+
const parsedSong = parser.parse(testFile);
71+
72+
expect(parsedSong.slideGroups[0].groupLabel).toEqual('');
73+
});
74+
2875
it('should get the data from "Be Near.pro5"', () => {
2976
const testFile = readFileSync('./sample-files/v5 - Be Near.pro5').toString();
3077
const parsedSong = parser.parse(testFile);

src/v5/parser.ts

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
IPro5SlideTextElement,
1111
IPro5Song,
1212
} from './parser.model';
13-
import { IXmlPro5Arrangement, IXmlPro5Doc, IXmlPro5DocRoot, IXmlPro5Slide, IXmlPro5SlideGroup } from './xml.model';
13+
import { IXmlPro5Doc, IXmlPro5DocRoot, IXmlPro5Slide, IXmlPro5SlideGroup } from './xml.model';
1414

1515
export class v5Parser {
1616
parse(fileContent: string): IPro5Song {
@@ -45,19 +45,19 @@ export class v5Parser {
4545

4646
const properties = this.getProperties(parsedDoc.RVPresentationDocument);
4747
const slideGroups = this.getSlideGroups(parsedDoc.RVPresentationDocument.groups.RVSlideGrouping);
48-
const arrangements = this.getArrangements(parsedDoc.RVPresentationDocument.arrangements.RVSongArrangement, slideGroups);
48+
const arrangements = this.getArrangements(parsedDoc.RVPresentationDocument, slideGroups);
4949

5050
return { properties, slideGroups, arrangements };
5151
}
5252

5353
private getProperties(xmlDoc: IXmlPro5Doc): IPro5Properties {
5454
return {
55-
CCLIArtistCredits: xmlDoc['@CCLIArtistCredits'],
56-
CCLICopyrightInfo: xmlDoc['@CCLICopyrightInfo'],
55+
CCLIArtistCredits: xmlDoc['@CCLIArtistCredits'] ?? '',
56+
CCLICopyrightInfo: xmlDoc['@CCLICopyrightInfo'] ?? '',
5757
CCLIDisplay: Boolean(xmlDoc['@CCLIDisplay']),
58-
CCLILicenseNumber: xmlDoc['@CCLILicenseNumber'],
59-
CCLIPublisher: xmlDoc['@CCLIPublisher'],
60-
CCLISongTitle: xmlDoc['@CCLISongTitle'],
58+
CCLILicenseNumber: xmlDoc['@CCLILicenseNumber'] ?? '',
59+
CCLIPublisher: xmlDoc['@CCLIPublisher'] ?? '',
60+
CCLISongTitle: xmlDoc['@CCLISongTitle'] ?? '',
6161
album: xmlDoc['@album'],
6262
artist: xmlDoc['@artist'],
6363
author: xmlDoc['@author'],
@@ -82,7 +82,7 @@ export class v5Parser {
8282
const groupColor = sg['@color'] === '' ? null : Utils.normalizeColorToRgbObj(sg['@color']);
8383
return {
8484
groupColor,
85-
groupLabel: sg['@name'],
85+
groupLabel: sg['@name'] ?? '',
8686
groupId: sg['@uuid'],
8787
slides: this.getSlidesForGroup(sg.slides.RVDisplaySlide),
8888
};
@@ -139,28 +139,30 @@ export class v5Parser {
139139
});
140140
}
141141

142-
private getArrangements(xmlArrangements: IXmlPro5Arrangement[], slideGroups: IPro5SlideGroup[]): IPro5Arrangement[] {
142+
private getArrangements(xmlDoc: IXmlPro5Doc, slideGroups: IPro5SlideGroup[]): IPro5Arrangement[] {
143143
const arrangementsArr: IPro5Arrangement[] = [];
144144

145-
for (const a of xmlArrangements) {
146-
arrangementsArr.push({
147-
color: Utils.normalizeColorToRgbObj(a['@color']),
148-
label: a['@name'],
149-
groupOrder: a.groupIDs.NSMutableString.map((group) => {
150-
//This should always find a match since you can't put something in an arrangement that doesn't already exist
151-
//So because of that it's OK to have a non-null assertion here
152-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
153-
const slideGroupMatch = slideGroups.find(
154-
//Look up the actual slide group by ID so we can get its name
155-
(sg) => sg.groupId === group['@serialization-native-value']
156-
)!;
145+
if (xmlDoc.arrangements?.RVSongArrangement) {
146+
for (const a of xmlDoc.arrangements.RVSongArrangement) {
147+
arrangementsArr.push({
148+
color: Utils.normalizeColorToRgbObj(a['@color']),
149+
label: a['@name'],
150+
groupOrder: a.groupIDs.NSMutableString.map((group) => {
151+
//This should always find a match since you can't put something in an arrangement that doesn't already exist
152+
//So because of that it's OK to have a non-null assertion here
153+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
154+
const slideGroupMatch = slideGroups.find(
155+
//Look up the actual slide group by ID so we can get its name
156+
(sg) => sg.groupId === group['@serialization-native-value']
157+
)!;
157158

158-
return {
159-
groupId: group['@serialization-native-value'],
160-
groupLabel: slideGroupMatch.groupLabel,
161-
};
162-
}),
163-
});
159+
return {
160+
groupId: group['@serialization-native-value'],
161+
groupLabel: slideGroupMatch.groupLabel,
162+
};
163+
}),
164+
});
165+
}
164166
}
165167

166168
return arrangementsArr;

src/v5/xml.model.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ export interface IXmlPro5DocRoot {
55
}
66

77
export interface IXmlPro5Doc {
8-
'@CCLIArtistCredits': string;
9-
'@CCLICopyrightInfo': string | number;
10-
'@CCLIDisplay': number;
11-
'@CCLILicenseNumber': string | number;
12-
'@CCLIPublisher': string;
13-
'@CCLISongTitle': string;
8+
'@CCLIArtistCredits'?: string;
9+
'@CCLICopyrightInfo'?: string | number;
10+
'@CCLIDisplay'?: number;
11+
'@CCLILicenseNumber'?: string | number;
12+
'@CCLIPublisher'?: string;
13+
'@CCLISongTitle'?: string;
1414
'@album': string;
1515
'@artist': string;
1616
'@author': string;
@@ -28,9 +28,9 @@ export interface IXmlPro5Doc {
2828
'@versionNumber': number;
2929
'@width': number;
3030

31-
arrangements: {
31+
arrangements?: {
3232
'@containerClass': 'NSMutableArray';
33-
RVSongArrangement: IXmlPro5Arrangement[];
33+
RVSongArrangement?: IXmlPro5Arrangement[];
3434
};
3535
groups: {
3636
'@containerClass': 'NSMutableArray';
@@ -98,7 +98,7 @@ export interface IXmlPro5ArrangementGroupId {
9898
//Slide Groups and Slides
9999

100100
export interface IXmlPro5SlideGroup {
101-
'@name': string;
101+
'@name'?: string;
102102
'@uuid': string;
103103
'@color': string;
104104
'@serialization-array-index': number;
@@ -230,6 +230,7 @@ export interface IXmlPro5ElementPosition {
230230
}
231231

232232
export interface IXmlPro5SlideElementStroke {
233+
'@containerClass': 'NSMutableDictionary';
233234
NSColor: {
234235
'@serialization-native-value': string;
235236
'@serialization-dictionary-key': 'RVShapeElementStrokeColorKey';
@@ -241,6 +242,7 @@ export interface IXmlPro5SlideElementStroke {
241242
}
242243

243244
export interface IXmlPro5SlideElementShadow {
245+
'@containerClass': 'NSMutableDictionary';
244246
NSMutableString: {
245247
'@serialization-native-value': string;
246248
'@serialization-dictionary-key': 'shadowOffset';

0 commit comments

Comments
 (0)