lyrics-structure 1.2.0 → 1.2.2
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/.prettierrc +10 -0
- package/.vscode/settings.json +19 -0
- package/CHANGELOG.md +14 -1
- package/debug.ts +47 -0
- package/dist/debug.d.ts +1 -0
- package/dist/debug.js +47 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +18 -0
- package/dist/lyrics.d.ts +25 -0
- package/dist/lyrics.js +91 -0
- package/dist/lyrics.test.d.ts +1 -0
- package/dist/lyrics.test.js +75 -0
- package/dist/slide.d.ts +10 -0
- package/dist/slide.js +53 -0
- package/dist/slide.test.d.ts +1 -0
- package/dist/slide.test.js +66 -0
- package/jest.config.js +7 -4
- package/lyrics.test.ts +54 -44
- package/lyrics.ts +80 -54
- package/package.json +4 -2
- package/slide.ts +53 -73
package/.prettierrc
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"editor.formatOnSave": true,
|
|
3
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
4
|
+
"editor.codeActionsOnSave": {
|
|
5
|
+
"source.fixAll": true
|
|
6
|
+
},
|
|
7
|
+
"[typescript]": {
|
|
8
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
9
|
+
},
|
|
10
|
+
"[javascript]": {
|
|
11
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
12
|
+
},
|
|
13
|
+
"[json]": {
|
|
14
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
15
|
+
},
|
|
16
|
+
"[markdown]": {
|
|
17
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
18
|
+
}
|
|
19
|
+
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.2] - 2024-04-06
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Fixed issue where consecutive lines without part containers were incorrectly split into separate parts
|
|
8
|
+
- Improved handling of regular text lines to maintain proper grouping
|
|
9
|
+
|
|
3
10
|
## [1.2.0] - 2024-03-XX
|
|
4
11
|
|
|
5
12
|
### Changed
|
|
13
|
+
|
|
6
14
|
- Simplified the lyrics format to use clear section markers
|
|
7
15
|
- Updated documentation with clearer examples
|
|
8
16
|
- Improved type definitions for better TypeScript support
|
|
9
17
|
|
|
10
18
|
### Added
|
|
19
|
+
|
|
11
20
|
- Support for section indications in parentheses
|
|
12
21
|
- Better handling of repeated sections
|
|
13
22
|
- More intuitive content structure
|
|
14
23
|
|
|
15
24
|
### Removed
|
|
25
|
+
|
|
16
26
|
- Slide-based content splitting
|
|
17
27
|
- Special command tags
|
|
18
28
|
- Complex formatting options
|
|
@@ -20,6 +30,7 @@
|
|
|
20
30
|
## [1.1.0] - 2024-03-XX
|
|
21
31
|
|
|
22
32
|
### Added
|
|
33
|
+
|
|
23
34
|
- Initial release
|
|
24
35
|
- `getParts` function for basic content extraction
|
|
25
36
|
- `getSlideParts` function for slide-based content splitting
|
|
@@ -28,6 +39,7 @@
|
|
|
28
39
|
- TypeScript types and documentation
|
|
29
40
|
|
|
30
41
|
### Features
|
|
42
|
+
|
|
31
43
|
- Bracketed section parsing
|
|
32
44
|
- Special command tag handling
|
|
33
45
|
- Slide splitting based on content length
|
|
@@ -37,7 +49,8 @@
|
|
|
37
49
|
- Unicode support in tags
|
|
38
50
|
|
|
39
51
|
### Test Coverage
|
|
52
|
+
|
|
40
53
|
- Basic functionality tests
|
|
41
54
|
- Slide splitting tests
|
|
42
55
|
- Special command tests
|
|
43
|
-
- Edge case handling
|
|
56
|
+
- Edge case handling
|
package/debug.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { getSlideParts } from './slide';
|
|
2
|
+
|
|
3
|
+
const comprehensiveInput = `
|
|
4
|
+
[verse]
|
|
5
|
+
First line of verse
|
|
6
|
+
Second line of verse
|
|
7
|
+
Third line of verse
|
|
8
|
+
Fourth line of verse
|
|
9
|
+
Fifth line of verse
|
|
10
|
+
[/verse]
|
|
11
|
+
[verse]
|
|
12
|
+
(inside parentheses)
|
|
13
|
+
Regular text line 1
|
|
14
|
+
Regular text line 2
|
|
15
|
+
|
|
16
|
+
[chorus]
|
|
17
|
+
First chorus line
|
|
18
|
+
Second chorus line
|
|
19
|
+
[/chorus]
|
|
20
|
+
|
|
21
|
+
[verse 1]
|
|
22
|
+
This is a very long line that should be considered too long for the slide
|
|
23
|
+
This is another very long line that should also be considered too long
|
|
24
|
+
Short line 1
|
|
25
|
+
Short line 2
|
|
26
|
+
[/verse 1]
|
|
27
|
+
|
|
28
|
+
Regular line 1
|
|
29
|
+
Regular line 2
|
|
30
|
+
|
|
31
|
+
More content
|
|
32
|
+
|
|
33
|
+
This is a very long line that exceeds forty characters for testing purposes
|
|
34
|
+
This is another very long line that also exceeds the forty character limit
|
|
35
|
+
Another very long line that should be considered too long for the slide
|
|
36
|
+
And yet another very long line that should be split into a new section
|
|
37
|
+
Regular line
|
|
38
|
+
|
|
39
|
+
[verse 2]
|
|
40
|
+
First line with spaces
|
|
41
|
+
Second line with spaces
|
|
42
|
+
[/verse 2]
|
|
43
|
+
|
|
44
|
+
[chorus]`;
|
|
45
|
+
|
|
46
|
+
console.log('Output from getSlideParts:');
|
|
47
|
+
console.log(JSON.stringify(getSlideParts(comprehensiveInput), null, 2));
|
package/dist/debug.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/debug.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const slide_1 = require("./slide");
|
|
4
|
+
const comprehensiveInput = `
|
|
5
|
+
[verse]
|
|
6
|
+
First line of verse
|
|
7
|
+
Second line of verse
|
|
8
|
+
Third line of verse
|
|
9
|
+
Fourth line of verse
|
|
10
|
+
Fifth line of verse
|
|
11
|
+
[/verse]
|
|
12
|
+
[verse]
|
|
13
|
+
(inside parentheses)
|
|
14
|
+
Regular text line 1
|
|
15
|
+
Regular text line 2
|
|
16
|
+
|
|
17
|
+
[chorus]
|
|
18
|
+
First chorus line
|
|
19
|
+
Second chorus line
|
|
20
|
+
[/chorus]
|
|
21
|
+
|
|
22
|
+
[verse 1]
|
|
23
|
+
This is a very long line that should be considered too long for the slide
|
|
24
|
+
This is another very long line that should also be considered too long
|
|
25
|
+
Short line 1
|
|
26
|
+
Short line 2
|
|
27
|
+
[/verse 1]
|
|
28
|
+
|
|
29
|
+
Regular line 1
|
|
30
|
+
Regular line 2
|
|
31
|
+
|
|
32
|
+
More content
|
|
33
|
+
|
|
34
|
+
This is a very long line that exceeds forty characters for testing purposes
|
|
35
|
+
This is another very long line that also exceeds the forty character limit
|
|
36
|
+
Another very long line that should be considered too long for the slide
|
|
37
|
+
And yet another very long line that should be split into a new section
|
|
38
|
+
Regular line
|
|
39
|
+
|
|
40
|
+
[verse 2]
|
|
41
|
+
First line with spaces
|
|
42
|
+
Second line with spaces
|
|
43
|
+
[/verse 2]
|
|
44
|
+
|
|
45
|
+
[chorus]`;
|
|
46
|
+
console.log('Output from getSlideParts:');
|
|
47
|
+
console.log(JSON.stringify((0, slide_1.getSlideParts)(comprehensiveInput), null, 2));
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./slide.js"), exports);
|
|
18
|
+
__exportStar(require("./lyrics.js"), exports);
|
package/dist/lyrics.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type LyricPart = {
|
|
2
|
+
name: string | undefined;
|
|
3
|
+
repetition: boolean;
|
|
4
|
+
indication: string | null;
|
|
5
|
+
content: string | undefined;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Splits lyrics into structured parts, handling named sections, repetitions, and indications.
|
|
9
|
+
* Works with lyrics formatted using square brackets for section names and optional parentheses for indications.
|
|
10
|
+
*
|
|
11
|
+
* Example input:
|
|
12
|
+
* ```
|
|
13
|
+
* [verse 1] (first time)
|
|
14
|
+
* Lyrics content here
|
|
15
|
+
* [/verse 1]
|
|
16
|
+
*
|
|
17
|
+
* [chorus]
|
|
18
|
+
* Chorus lyrics
|
|
19
|
+
* [/chorus]
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @param lyrics - The input lyrics text to be parsed into parts
|
|
23
|
+
* @returns Array of LyricPart objects containing structured lyrics data
|
|
24
|
+
*/
|
|
25
|
+
export declare function getLyricsParts(lyrics: string): LyricPart[];
|
package/dist/lyrics.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getLyricsParts = getLyricsParts;
|
|
4
|
+
/**
|
|
5
|
+
* Splits lyrics into structured parts, handling named sections, repetitions, and indications.
|
|
6
|
+
* Works with lyrics formatted using square brackets for section names and optional parentheses for indications.
|
|
7
|
+
*
|
|
8
|
+
* Example input:
|
|
9
|
+
* ```
|
|
10
|
+
* [verse 1] (first time)
|
|
11
|
+
* Lyrics content here
|
|
12
|
+
* [/verse 1]
|
|
13
|
+
*
|
|
14
|
+
* [chorus]
|
|
15
|
+
* Chorus lyrics
|
|
16
|
+
* [/chorus]
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* @param lyrics - The input lyrics text to be parsed into parts
|
|
20
|
+
* @returns Array of LyricPart objects containing structured lyrics data
|
|
21
|
+
*/
|
|
22
|
+
function getLyricsParts(lyrics) {
|
|
23
|
+
const parts = [];
|
|
24
|
+
const seenParts = new Map();
|
|
25
|
+
const partContentMap = new Map();
|
|
26
|
+
// First pass: extract all content with closing tags
|
|
27
|
+
const cleanedText = lyrics.replace(/\[([^\]]+)\](?:\s*\(([^)]+)\))?\s*([\s\S]*?)\[\/\1\]/g, (match, key, indication, content) => {
|
|
28
|
+
if (!partContentMap.has(key)) {
|
|
29
|
+
partContentMap.set(key, content.trim());
|
|
30
|
+
}
|
|
31
|
+
return `[${key}]${indication ? ` (${indication})` : ''}`;
|
|
32
|
+
});
|
|
33
|
+
// Split the lyrics into lines and process them
|
|
34
|
+
const lines = cleanedText.split('\n');
|
|
35
|
+
let currentPart = null;
|
|
36
|
+
let currentUnnamedContent = [];
|
|
37
|
+
for (let i = 0; i < lines.length; i++) {
|
|
38
|
+
const line = lines[i].trim();
|
|
39
|
+
// Check for part start
|
|
40
|
+
const partStartMatch = line.match(/^\[([^\]]+)\](?:\s*\(([^)]+)\))?$/);
|
|
41
|
+
if (partStartMatch) {
|
|
42
|
+
// If we have accumulated unnamed content, add it as a part
|
|
43
|
+
if (currentUnnamedContent.length > 0) {
|
|
44
|
+
parts.push({
|
|
45
|
+
name: undefined,
|
|
46
|
+
repetition: false,
|
|
47
|
+
indication: null,
|
|
48
|
+
content: currentUnnamedContent.join('\n'),
|
|
49
|
+
});
|
|
50
|
+
currentUnnamedContent = [];
|
|
51
|
+
}
|
|
52
|
+
const partName = partStartMatch[1];
|
|
53
|
+
const indication = partStartMatch[2] || null;
|
|
54
|
+
// Check if this part has been seen before
|
|
55
|
+
const partCount = (seenParts.get(partName) || 0) + 1;
|
|
56
|
+
seenParts.set(partName, partCount);
|
|
57
|
+
currentPart = {
|
|
58
|
+
name: partName,
|
|
59
|
+
repetition: partCount > 1,
|
|
60
|
+
indication,
|
|
61
|
+
content: partContentMap.get(partName),
|
|
62
|
+
};
|
|
63
|
+
parts.push(currentPart);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
// Handle content without a part container
|
|
67
|
+
if (line && !line.startsWith('[') && !line.startsWith('[/')) {
|
|
68
|
+
currentUnnamedContent.push(line);
|
|
69
|
+
}
|
|
70
|
+
else if (currentUnnamedContent.length > 0) {
|
|
71
|
+
// If we hit a part marker or empty line, add the accumulated content
|
|
72
|
+
parts.push({
|
|
73
|
+
name: undefined,
|
|
74
|
+
repetition: false,
|
|
75
|
+
indication: null,
|
|
76
|
+
content: currentUnnamedContent.join('\n'),
|
|
77
|
+
});
|
|
78
|
+
currentUnnamedContent = [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Add any remaining unnamed content
|
|
82
|
+
if (currentUnnamedContent.length > 0) {
|
|
83
|
+
parts.push({
|
|
84
|
+
name: undefined,
|
|
85
|
+
repetition: false,
|
|
86
|
+
indication: null,
|
|
87
|
+
content: currentUnnamedContent.join('\n'),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return parts;
|
|
91
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const lyrics_1 = require("./lyrics");
|
|
4
|
+
const testLyrics = `[partname 1] (indication 1)
|
|
5
|
+
|
|
6
|
+
content 1
|
|
7
|
+
|
|
8
|
+
[/partname 1]
|
|
9
|
+
|
|
10
|
+
[partname 1] (indication 2)
|
|
11
|
+
|
|
12
|
+
[partname 2]
|
|
13
|
+
|
|
14
|
+
content 2
|
|
15
|
+
|
|
16
|
+
[/partname 2]
|
|
17
|
+
|
|
18
|
+
[interlude 1]
|
|
19
|
+
|
|
20
|
+
[partname 3]
|
|
21
|
+
|
|
22
|
+
content without partname container
|
|
23
|
+
|
|
24
|
+
content standalone 1
|
|
25
|
+
content standalone 2
|
|
26
|
+
`;
|
|
27
|
+
describe('getLyricsParts', () => {
|
|
28
|
+
it('should correctly parse lyrics into parts', () => {
|
|
29
|
+
const result = (0, lyrics_1.getLyricsParts)(testLyrics);
|
|
30
|
+
expect(result).toEqual([
|
|
31
|
+
{
|
|
32
|
+
name: 'partname 1',
|
|
33
|
+
repetition: false,
|
|
34
|
+
indication: 'indication 1',
|
|
35
|
+
content: 'content 1',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'partname 1',
|
|
39
|
+
repetition: true,
|
|
40
|
+
indication: 'indication 2',
|
|
41
|
+
content: 'content 1',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'partname 2',
|
|
45
|
+
repetition: false,
|
|
46
|
+
indication: null,
|
|
47
|
+
content: 'content 2',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'interlude 1',
|
|
51
|
+
repetition: false,
|
|
52
|
+
indication: null,
|
|
53
|
+
content: undefined,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'partname 3',
|
|
57
|
+
repetition: false,
|
|
58
|
+
indication: null,
|
|
59
|
+
content: undefined,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: undefined,
|
|
63
|
+
repetition: false,
|
|
64
|
+
indication: null,
|
|
65
|
+
content: 'content without partname container',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: undefined,
|
|
69
|
+
repetition: false,
|
|
70
|
+
indication: null,
|
|
71
|
+
content: 'content standalone 1\ncontent standalone 2',
|
|
72
|
+
},
|
|
73
|
+
]);
|
|
74
|
+
});
|
|
75
|
+
});
|
package/dist/slide.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Splits text into natural sections based on text structure.
|
|
3
|
+
* Works with plain text without requiring markdown or special formatting.
|
|
4
|
+
* Empty lines are treated as natural separators between parts.
|
|
5
|
+
*
|
|
6
|
+
* @param text - The input text to be split into sections
|
|
7
|
+
* @param maxLinesPerSlide - Maximum number of lines to include in a single slide (default: 6)
|
|
8
|
+
* @returns An array of content sections
|
|
9
|
+
*/
|
|
10
|
+
export declare const getSlideParts: (text?: string) => string[];
|
package/dist/slide.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Splits text into natural sections based on text structure.
|
|
4
|
+
* Works with plain text without requiring markdown or special formatting.
|
|
5
|
+
* Empty lines are treated as natural separators between parts.
|
|
6
|
+
*
|
|
7
|
+
* @param text - The input text to be split into sections
|
|
8
|
+
* @param maxLinesPerSlide - Maximum number of lines to include in a single slide (default: 6)
|
|
9
|
+
* @returns An array of content sections
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.getSlideParts = void 0;
|
|
13
|
+
const lyrics_1 = require("./lyrics");
|
|
14
|
+
const isLineTooLong = (line) => line.length > 40;
|
|
15
|
+
const processContent = (content) => {
|
|
16
|
+
const slides = [];
|
|
17
|
+
let currentSlide = [];
|
|
18
|
+
content.split('\n').forEach((line) => {
|
|
19
|
+
const trimmedLine = line.trim();
|
|
20
|
+
if (!trimmedLine) {
|
|
21
|
+
if (currentSlide.length > 0) {
|
|
22
|
+
slides.push(currentSlide.join('\n'));
|
|
23
|
+
currentSlide = [];
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (currentSlide.length >= 4 ||
|
|
28
|
+
(currentSlide.length >= 2 && currentSlide.filter(isLineTooLong).length >= 2)) {
|
|
29
|
+
slides.push(currentSlide.join('\n'));
|
|
30
|
+
currentSlide = [];
|
|
31
|
+
}
|
|
32
|
+
currentSlide.push(trimmedLine);
|
|
33
|
+
});
|
|
34
|
+
if (currentSlide.length > 0) {
|
|
35
|
+
slides.push(currentSlide.join('\n'));
|
|
36
|
+
}
|
|
37
|
+
return slides;
|
|
38
|
+
};
|
|
39
|
+
const getSlideParts = (text) => {
|
|
40
|
+
if (!text)
|
|
41
|
+
return [];
|
|
42
|
+
// Remove text between parentheses
|
|
43
|
+
const textWithoutParentheses = text.replace(/\([^)]*\)/g, '');
|
|
44
|
+
const partsText = (0, lyrics_1.getLyricsParts)(textWithoutParentheses).map((part) => part.content);
|
|
45
|
+
const result = [];
|
|
46
|
+
partsText.forEach((part) => {
|
|
47
|
+
if (part) {
|
|
48
|
+
result.push(...processContent(part));
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
return result;
|
|
52
|
+
};
|
|
53
|
+
exports.getSlideParts = getSlideParts;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const slide_js_1 = require("./slide.js");
|
|
4
|
+
describe('Comprehensive Tests', () => {
|
|
5
|
+
test('handles all cases in one comprehensive input', () => {
|
|
6
|
+
const comprehensiveInput = `
|
|
7
|
+
[verse]
|
|
8
|
+
First line of verse
|
|
9
|
+
Second line of verse
|
|
10
|
+
Third line of verse
|
|
11
|
+
Fourth line of verse
|
|
12
|
+
Fifth line of verse
|
|
13
|
+
[/verse]
|
|
14
|
+
[verse]
|
|
15
|
+
(inside parentheses)
|
|
16
|
+
Regular text line 1
|
|
17
|
+
Regular text line 2
|
|
18
|
+
|
|
19
|
+
[chorus]
|
|
20
|
+
First chorus line
|
|
21
|
+
Second chorus line
|
|
22
|
+
[/chorus]
|
|
23
|
+
|
|
24
|
+
[verse 1]
|
|
25
|
+
This is a very long line that should be considered too long for the slide
|
|
26
|
+
This is another very long line that should also be considered too long
|
|
27
|
+
Short line 1
|
|
28
|
+
Short line 2
|
|
29
|
+
[/verse 1]
|
|
30
|
+
|
|
31
|
+
Regular line 1
|
|
32
|
+
Regular line 2
|
|
33
|
+
|
|
34
|
+
More content
|
|
35
|
+
|
|
36
|
+
This is a very long line that exceeds forty characters for testing purposes
|
|
37
|
+
This is another very long line that also exceeds the forty character limit
|
|
38
|
+
Another very long line that should be considered too long for the slide
|
|
39
|
+
And yet another very long line that should be split into a new section
|
|
40
|
+
Regular line
|
|
41
|
+
|
|
42
|
+
[verse 2]
|
|
43
|
+
First line with spaces
|
|
44
|
+
Second line with spaces
|
|
45
|
+
[/verse 2]
|
|
46
|
+
|
|
47
|
+
[chorus]`;
|
|
48
|
+
expect((0, slide_js_1.getSlideParts)(comprehensiveInput)).toEqual([
|
|
49
|
+
'First line of verse\nSecond line of verse\nThird line of verse\nFourth line of verse',
|
|
50
|
+
'Fifth line of verse',
|
|
51
|
+
'First line of verse\nSecond line of verse\nThird line of verse\nFourth line of verse',
|
|
52
|
+
'Fifth line of verse',
|
|
53
|
+
'Regular text line 1\nRegular text line 2',
|
|
54
|
+
'First chorus line\nSecond chorus line',
|
|
55
|
+
'This is a very long line that should be considered too long for the slide\nThis is another very long line that should also be considered too long',
|
|
56
|
+
'Short line 1\nShort line 2',
|
|
57
|
+
'Regular line 1\nRegular line 2',
|
|
58
|
+
'More content',
|
|
59
|
+
'This is a very long line that exceeds forty characters for testing purposes\nThis is another very long line that also exceeds the forty character limit',
|
|
60
|
+
'Another very long line that should be considered too long for the slide\nAnd yet another very long line that should be split into a new section',
|
|
61
|
+
'Regular line',
|
|
62
|
+
'First line with spaces\nSecond line with spaces',
|
|
63
|
+
'First chorus line\nSecond chorus line'
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
});
|
package/jest.config.js
CHANGED
package/lyrics.test.ts
CHANGED
|
@@ -18,49 +18,59 @@ content 2
|
|
|
18
18
|
|
|
19
19
|
[partname 3]
|
|
20
20
|
|
|
21
|
-
content without partname container
|
|
21
|
+
content without partname container
|
|
22
|
+
|
|
23
|
+
content standalone 1
|
|
24
|
+
content standalone 2
|
|
25
|
+
`;
|
|
22
26
|
|
|
23
27
|
describe('getLyricsParts', () => {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
28
|
+
it('should correctly parse lyrics into parts', () => {
|
|
29
|
+
const result = getLyricsParts(testLyrics);
|
|
30
|
+
|
|
31
|
+
expect(result).toEqual([
|
|
32
|
+
{
|
|
33
|
+
name: 'partname 1',
|
|
34
|
+
repetition: false,
|
|
35
|
+
indication: 'indication 1',
|
|
36
|
+
content: 'content 1',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'partname 1',
|
|
40
|
+
repetition: true,
|
|
41
|
+
indication: 'indication 2',
|
|
42
|
+
content: 'content 1',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'partname 2',
|
|
46
|
+
repetition: false,
|
|
47
|
+
indication: null,
|
|
48
|
+
content: 'content 2',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'interlude 1',
|
|
52
|
+
repetition: false,
|
|
53
|
+
indication: null,
|
|
54
|
+
content: undefined,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'partname 3',
|
|
58
|
+
repetition: false,
|
|
59
|
+
indication: null,
|
|
60
|
+
content: undefined,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: undefined,
|
|
64
|
+
repetition: false,
|
|
65
|
+
indication: null,
|
|
66
|
+
content: 'content without partname container',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: undefined,
|
|
70
|
+
repetition: false,
|
|
71
|
+
indication: null,
|
|
72
|
+
content: 'content standalone 1\ncontent standalone 2',
|
|
73
|
+
},
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
});
|
package/lyrics.ts
CHANGED
|
@@ -1,81 +1,107 @@
|
|
|
1
1
|
export type LyricPart = {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
name: string | undefined;
|
|
3
|
+
repetition: boolean;
|
|
4
|
+
indication: string | null;
|
|
5
|
+
content: string | undefined;
|
|
6
6
|
};
|
|
7
7
|
/**
|
|
8
8
|
* Splits lyrics into structured parts, handling named sections, repetitions, and indications.
|
|
9
9
|
* Works with lyrics formatted using square brackets for section names and optional parentheses for indications.
|
|
10
|
-
*
|
|
10
|
+
*
|
|
11
11
|
* Example input:
|
|
12
12
|
* ```
|
|
13
13
|
* [verse 1] (first time)
|
|
14
14
|
* Lyrics content here
|
|
15
15
|
* [/verse 1]
|
|
16
|
-
*
|
|
16
|
+
*
|
|
17
17
|
* [chorus]
|
|
18
18
|
* Chorus lyrics
|
|
19
19
|
* [/chorus]
|
|
20
20
|
* ```
|
|
21
|
-
*
|
|
21
|
+
*
|
|
22
22
|
* @param lyrics - The input lyrics text to be parsed into parts
|
|
23
23
|
* @returns Array of LyricPart objects containing structured lyrics data
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
export function getLyricsParts(lyrics: string): LyricPart[] {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
const parts: LyricPart[] = [];
|
|
28
|
+
const seenParts = new Map<string, number>();
|
|
29
|
+
const partContentMap = new Map<string, string>();
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
31
|
+
// First pass: extract all content with closing tags
|
|
32
|
+
const cleanedText = lyrics.replace(
|
|
33
|
+
/\[([^\]]+)\](?:\s*\(([^)]+)\))?\s*([\s\S]*?)\[\/\1\]/g,
|
|
34
|
+
(match, key: string, indication: string | undefined, content: string) => {
|
|
35
|
+
if (!partContentMap.has(key)) {
|
|
36
|
+
partContentMap.set(key, content.trim());
|
|
37
|
+
}
|
|
38
|
+
return `[${key}]${indication ? ` (${indication})` : ''}`;
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Split the lyrics into lines and process them
|
|
43
|
+
const lines = cleanedText.split('\n');
|
|
44
|
+
let currentPart: LyricPart | null = null;
|
|
45
|
+
let currentUnnamedContent: string[] = [];
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
let currentPart: LyricPart | null = null;
|
|
47
|
+
for (let i = 0; i < lines.length; i++) {
|
|
48
|
+
const line = lines[i].trim();
|
|
45
49
|
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
// Check for part start
|
|
51
|
+
const partStartMatch = line.match(/^\[([^\]]+)\](?:\s*\(([^)]+)\))?$/);
|
|
52
|
+
if (partStartMatch) {
|
|
53
|
+
// If we have accumulated unnamed content, add it as a part
|
|
54
|
+
if (currentUnnamedContent.length > 0) {
|
|
55
|
+
parts.push({
|
|
56
|
+
name: undefined,
|
|
57
|
+
repetition: false,
|
|
58
|
+
indication: null,
|
|
59
|
+
content: currentUnnamedContent.join('\n'),
|
|
60
|
+
});
|
|
61
|
+
currentUnnamedContent = [];
|
|
62
|
+
}
|
|
48
63
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (partStartMatch) {
|
|
52
|
-
const partName = partStartMatch[1];
|
|
53
|
-
const indication = partStartMatch[2] || null;
|
|
54
|
-
|
|
55
|
-
// Check if this part has been seen before
|
|
56
|
-
const partCount = (seenParts.get(partName) || 0) + 1;
|
|
57
|
-
seenParts.set(partName, partCount);
|
|
64
|
+
const partName = partStartMatch[1];
|
|
65
|
+
const indication = partStartMatch[2] || null;
|
|
58
66
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
indication,
|
|
63
|
-
content: partContentMap.get(partName)
|
|
64
|
-
};
|
|
65
|
-
parts.push(currentPart);
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
67
|
+
// Check if this part has been seen before
|
|
68
|
+
const partCount = (seenParts.get(partName) || 0) + 1;
|
|
69
|
+
seenParts.set(partName, partCount);
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
71
|
+
currentPart = {
|
|
72
|
+
name: partName,
|
|
73
|
+
repetition: partCount > 1,
|
|
74
|
+
indication,
|
|
75
|
+
content: partContentMap.get(partName),
|
|
76
|
+
};
|
|
77
|
+
parts.push(currentPart);
|
|
78
|
+
continue;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
// Handle content without a part container
|
|
82
|
+
if (line && !line.startsWith('[') && !line.startsWith('[/')) {
|
|
83
|
+
currentUnnamedContent.push(line);
|
|
84
|
+
} else if (currentUnnamedContent.length > 0) {
|
|
85
|
+
// If we hit a part marker or empty line, add the accumulated content
|
|
86
|
+
parts.push({
|
|
87
|
+
name: undefined,
|
|
88
|
+
repetition: false,
|
|
89
|
+
indication: null,
|
|
90
|
+
content: currentUnnamedContent.join('\n'),
|
|
91
|
+
});
|
|
92
|
+
currentUnnamedContent = [];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Add any remaining unnamed content
|
|
97
|
+
if (currentUnnamedContent.length > 0) {
|
|
98
|
+
parts.push({
|
|
99
|
+
name: undefined,
|
|
100
|
+
repetition: false,
|
|
101
|
+
indication: null,
|
|
102
|
+
content: currentUnnamedContent.join('\n'),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return parts;
|
|
107
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lyrics-structure",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Parser for lyrics with structured sections, names, and indications",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc",
|
|
9
9
|
"test": "jest",
|
|
10
|
-
"test:watch": "jest --watch"
|
|
10
|
+
"test:watch": "jest --watch",
|
|
11
|
+
"format": "prettier --write \"**/*.{ts,js,json,md}\""
|
|
11
12
|
},
|
|
12
13
|
"keywords": [
|
|
13
14
|
"lyrics",
|
|
@@ -20,6 +21,7 @@
|
|
|
20
21
|
"devDependencies": {
|
|
21
22
|
"@types/jest": "^29.0.0",
|
|
22
23
|
"jest": "^29.0.0",
|
|
24
|
+
"prettier": "^3.5.3",
|
|
23
25
|
"ts-jest": "^29.0.0",
|
|
24
26
|
"typescript": "^5.0.0"
|
|
25
27
|
}
|
package/slide.ts
CHANGED
|
@@ -2,82 +2,62 @@
|
|
|
2
2
|
* Splits text into natural sections based on text structure.
|
|
3
3
|
* Works with plain text without requiring markdown or special formatting.
|
|
4
4
|
* Empty lines are treated as natural separators between parts.
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
6
|
* @param text - The input text to be split into sections
|
|
7
7
|
* @param maxLinesPerSlide - Maximum number of lines to include in a single slide (default: 6)
|
|
8
8
|
* @returns An array of content sections
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
if (!text) return [];
|
|
13
|
-
|
|
14
|
-
const isLineTooLong = (line: string) => line.length > 40;
|
|
15
|
-
|
|
16
|
-
// Process parts in brackets and create a map
|
|
17
|
-
const partsMap = new Map<string, string>();
|
|
18
|
-
|
|
19
|
-
// First pass: extract all content with closing tags
|
|
20
|
-
const cleanedText = text.replace(
|
|
21
|
-
/\[(.*?)\]([\s\S]*?)\[\/\1\]/g,
|
|
22
|
-
(match, key: string, content) => {
|
|
23
|
-
if (!partsMap.has(key)) {
|
|
24
|
-
partsMap.set(key, content.trim());
|
|
25
|
-
}
|
|
26
|
-
return `[${key}]`;
|
|
27
|
-
}
|
|
28
|
-
);
|
|
11
|
+
import { getLyricsParts } from './lyrics';
|
|
29
12
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return result;
|
|
83
|
-
};
|
|
13
|
+
const isLineTooLong = (line: string) => line.length > 40;
|
|
14
|
+
const processContent = (content: string): string[] => {
|
|
15
|
+
const slides: string[] = [];
|
|
16
|
+
let currentSlide: string[] = [];
|
|
17
|
+
|
|
18
|
+
content.split('\n').forEach((line) => {
|
|
19
|
+
const trimmedLine = line.trim();
|
|
20
|
+
|
|
21
|
+
if (!trimmedLine) {
|
|
22
|
+
if (currentSlide.length > 0) {
|
|
23
|
+
slides.push(currentSlide.join('\n'));
|
|
24
|
+
currentSlide = [];
|
|
25
|
+
}
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (
|
|
30
|
+
currentSlide.length >= 4 ||
|
|
31
|
+
(currentSlide.length >= 2 && currentSlide.filter(isLineTooLong).length >= 2)
|
|
32
|
+
) {
|
|
33
|
+
slides.push(currentSlide.join('\n'));
|
|
34
|
+
currentSlide = [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
currentSlide.push(trimmedLine);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (currentSlide.length > 0) {
|
|
41
|
+
slides.push(currentSlide.join('\n'));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return slides;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const getSlideParts = (text?: string): string[] => {
|
|
48
|
+
if (!text) return [];
|
|
49
|
+
|
|
50
|
+
// Remove text between parentheses
|
|
51
|
+
const textWithoutParentheses = text.replace(/\([^)]*\)/g, '');
|
|
52
|
+
const partsText = getLyricsParts(textWithoutParentheses).map((part) => part.content);
|
|
53
|
+
|
|
54
|
+
const result: string[] = [];
|
|
55
|
+
|
|
56
|
+
partsText.forEach((part) => {
|
|
57
|
+
if (part) {
|
|
58
|
+
result.push(...processContent(part));
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return result;
|
|
63
|
+
};
|