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 ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "semi": true,
3
+ "trailingComma": "es5",
4
+ "singleQuote": true,
5
+ "printWidth": 100,
6
+ "tabWidth": 2,
7
+ "useTabs": false,
8
+ "bracketSpacing": true,
9
+ "arrowParens": "always"
10
+ }
@@ -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));
@@ -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));
@@ -0,0 +1,2 @@
1
+ export * from './slide.js';
2
+ export * from './lyrics.js';
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);
@@ -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
+ });
@@ -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
@@ -7,8 +7,11 @@ export default {
7
7
  '^(\\.{1,2}/.*)\\.js$': '$1',
8
8
  },
9
9
  transform: {
10
- '^.+\\.tsx?$': ['ts-jest', {
11
- useESM: true,
12
- }],
10
+ '^.+\\.tsx?$': [
11
+ 'ts-jest',
12
+ {
13
+ useESM: true,
14
+ },
15
+ ],
13
16
  },
14
- };
17
+ };
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
- it('should correctly parse lyrics into parts', () => {
25
- const result = getLyricsParts(testLyrics);
26
-
27
- expect(result).toEqual([
28
- {
29
- name: 'partname 1',
30
- repetition: false,
31
- indication: 'indication 1',
32
- content: 'content 1'
33
- },
34
- {
35
- name: 'partname 1',
36
- repetition: true,
37
- indication: 'indication 2',
38
- content: 'content 1'
39
- },
40
- {
41
- name: 'partname 2',
42
- repetition: false,
43
- indication: null,
44
- content: 'content 2'
45
- },
46
- {
47
- name: 'interlude 1',
48
- repetition: false,
49
- indication: null,
50
- content: undefined
51
- },
52
- {
53
- name: 'partname 3',
54
- repetition: false,
55
- indication: null,
56
- content: undefined
57
- },
58
- {
59
- name: undefined,
60
- repetition: false,
61
- indication: null,
62
- content: 'content without partname container'
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
- name: string | undefined;
3
- repetition: boolean;
4
- indication: string | null;
5
- content: string | undefined;
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
- const parts: LyricPart[] = [];
28
- const seenParts = new Map<string, number>();
29
- const partContentMap = new Map<string, string>();
27
+ const parts: LyricPart[] = [];
28
+ const seenParts = new Map<string, number>();
29
+ const partContentMap = new Map<string, string>();
30
30
 
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
- );
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
- // Split the lyrics into lines and process them
43
- const lines = cleanedText.split('\n');
44
- let currentPart: LyricPart | null = null;
47
+ for (let i = 0; i < lines.length; i++) {
48
+ const line = lines[i].trim();
45
49
 
46
- for (let i = 0; i < lines.length; i++) {
47
- const line = lines[i].trim();
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
- // Check for part start
50
- const partStartMatch = line.match(/^\[([^\]]+)\](?:\s*\(([^)]+)\))?$/);
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
- currentPart = {
60
- name: partName,
61
- repetition: partCount > 1,
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
- // Handle content without a part container
70
- if (line && !line.startsWith('[') && !line.startsWith('[/')) {
71
- parts.push({
72
- name: undefined,
73
- repetition: false,
74
- indication: null,
75
- content: line
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
- return parts;
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.0",
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
- export const getSlideParts = (text?: string): string[] => {
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
- // Remove text between parentheses
31
- const textWithoutParentheses = cleanedText.replace(/\([^)]*\)/g, '');
32
-
33
- const processContent = (content: string): string[] => {
34
- const slides: string[] = [];
35
- let currentSlide: string[] = [];
36
-
37
- content.split("\n").forEach((line) => {
38
- const trimmedLine = line.trim();
39
-
40
- if (!trimmedLine) {
41
- if (currentSlide.length > 0) {
42
- slides.push(currentSlide.join("\n"));
43
- currentSlide = [];
44
- }
45
- return;
46
- }
47
-
48
- if (
49
- currentSlide.length >= 4 ||
50
- (currentSlide.length >= 2 &&
51
- currentSlide.filter(isLineTooLong).length >= 2)
52
- ) {
53
- slides.push(currentSlide.join("\n"));
54
- currentSlide = [];
55
- }
56
-
57
- currentSlide.push(trimmedLine);
58
- });
59
-
60
- if (currentSlide.length > 0) {
61
- slides.push(currentSlide.join("\n"));
62
- }
63
-
64
- return slides;
65
- };
66
-
67
- const result: string[] = [];
68
- const parts = textWithoutParentheses
69
- .trim()
70
- .split(/\[([^\]]+)\]/)
71
- .filter(Boolean);
72
-
73
- parts.forEach((part) => {
74
- if (partsMap.has(part)) {
75
- const partContent = partsMap.get(part)!;
76
- result.push(...processContent(partContent));
77
- } else if (part.trim()) {
78
- result.push(...processContent(part));
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
+ };