lyrics-structure 1.0.6 → 1.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,43 @@
1
+ # Changelog
2
+
3
+ ## [1.2.0] - 2024-03-XX
4
+
5
+ ### Changed
6
+ - Simplified the lyrics format to use clear section markers
7
+ - Updated documentation with clearer examples
8
+ - Improved type definitions for better TypeScript support
9
+
10
+ ### Added
11
+ - Support for section indications in parentheses
12
+ - Better handling of repeated sections
13
+ - More intuitive content structure
14
+
15
+ ### Removed
16
+ - Slide-based content splitting
17
+ - Special command tags
18
+ - Complex formatting options
19
+
20
+ ## [1.1.0] - 2024-03-XX
21
+
22
+ ### Added
23
+ - Initial release
24
+ - `getParts` function for basic content extraction
25
+ - `getSlideParts` function for slide-based content splitting
26
+ - Support for special command tags
27
+ - Comprehensive test suite
28
+ - TypeScript types and documentation
29
+
30
+ ### Features
31
+ - Bracketed section parsing
32
+ - Special command tag handling
33
+ - Slide splitting based on content length
34
+ - Empty line separation
35
+ - Long line handling (>40 characters)
36
+ - Whitespace preservation
37
+ - Unicode support in tags
38
+
39
+ ### Test Coverage
40
+ - Basic functionality tests
41
+ - Slide splitting tests
42
+ - Special command tests
43
+ - Edge case handling
package/README.md CHANGED
@@ -1,84 +1,54 @@
1
- # Lyrics Structure
2
-
3
- A utility library for structuring and formatting song lyrics and text content.
4
-
5
- ## Features
6
-
7
- - Split text into natural sections based on structure
8
- - Process bracketed content while maintaining structure
9
- - Intelligent text segmentation for readability
10
- - Efficient handling of repeated sections with simple tag references
11
-
12
- ## Usage
13
-
14
- ```typescript
15
- import { getParts, getSlideParts } from 'lyrics-structure';
16
-
17
- // Split text into parts based on bracketed content
18
- const parts = getParts(text);
19
-
20
- // Split text into natural sections for presentation
21
- const slideParts = getSlideParts(text, maxLinesPerSlide);
22
- ```
23
-
24
- ## Bracketed Content Format
25
-
26
- When structuring your lyrics or text content:
27
- - Define a section with both opening and closing tags: `[section]content[/section]`
28
- - For repeated sections, simply use the tag name again: `[section]`
29
- - The library will automatically reuse the content from the first definition
30
-
31
- This approach eliminates redundancy and makes lyric management much simpler, especially for songs with repeated choruses or sections.
32
-
33
- ## Example
34
-
35
- ```typescript
36
- // Example with bracketed content
37
- const lyrics = `[verse1]
38
- Morning light breaks through my window
39
- Another day to find my way
40
- The journey starts with just one step
41
- I'm moving forward, come what may
42
- [/verse1]
43
-
44
- [chorus]
45
- This is the moment, this is the time
46
- Hearts united, rhythm and rhyme
47
- Together we rise, together we stand
48
- Voices in harmony across the land
49
- [/chorus]
50
-
51
- [verse2]
52
- Challenges come and challenges go
53
- But strength inside continues to grow
54
- With every obstacle that I face
55
- I find my courage, I find my place
56
- [/verse2]
57
-
58
- [chorus]`;
59
-
60
- // Get the individual parts (verses and chorus)
61
- const parts = getParts(lyrics);
62
- console.log(parts);
63
- // Output will be an array with the content of verse1, chorus, verse2, and chorus again
64
- // Note that [chorus] is referenced twice but only defined once
65
-
66
- // Get slide-friendly sections
67
- const slides = getSlideParts(lyrics, 4);
68
- console.log(slides);
69
- // Output will break the content into slide-sized chunks with at most 4 lines each
70
- ```
71
-
72
- ## Implementation
73
-
74
- This library is used in the Stage app ([stage.loha.dev](https://stage.loha.dev)) for lyrics display and presentation.
75
-
76
- ## Installation
77
-
78
- ```bash
79
- npm install lyrics-structure
80
- ```
81
-
82
- ## License
83
-
84
- ISC
1
+ # Lyrics Parser
2
+
3
+ A TypeScript library for parsing lyrics with bracketed sections and special commands. Supports splitting text into structured parts with names and indications.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install lyrics-parser
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { getLyricsParts } from 'lyrics-parser';
15
+
16
+ const lyrics = `[verse 1] (first time)
17
+ This is the first verse content
18
+ [/verse 1]
19
+
20
+ [chorus]
21
+ This is the chorus content
22
+ [/chorus]
23
+
24
+ [verse 2]
25
+ This is the second verse content
26
+ [/verse 2]`;
27
+
28
+ const parts = getLyricsParts(lyrics);
29
+ // Result: Array of LyricPart objects with name, repetition, indication, and content
30
+ ```
31
+
32
+ ## Features
33
+
34
+ - Extracts content from bracketed sections
35
+ - Handles section names and indications
36
+ - Supports repeated sections
37
+ - Preserves non-bracketed content
38
+ - TypeScript support
39
+
40
+ ## Example Format
41
+
42
+ ```text
43
+ [partname] (indication)
44
+ content
45
+ [/partname]
46
+
47
+ [another part]
48
+ more content
49
+ [/another part]
50
+ ```
51
+
52
+ ## License
53
+
54
+ MIT
package/index.ts CHANGED
@@ -1,175 +1,2 @@
1
- /**
2
- * Splits text into parts based on bracketed content while maintaining its structure.
3
- * Does not consider line length or formatting.
4
- *
5
- * @param text - The input text to be split into parts
6
- * @returns An array of content strings
7
- */
8
- export const getParts = (text?: string): string[] => {
9
- if (!text) return [];
10
-
11
- // Process parts in brackets and create a map
12
- const partsMap = new Map<string, string>();
13
-
14
- // First pass: extract all content with closing tags
15
- const cleanedText = text.replace(
16
- /\[(.*?)\]([\s\S]*?)\[\/\1\]/g,
17
- (match, key, content) => {
18
- if (!partsMap.has(key)) {
19
- partsMap.set(key, content.trim());
20
- }
21
- return `[${key}]`;
22
- },
23
- );
24
-
25
- // Second pass: handle validation and solo tags that should reuse content
26
- const processedText = cleanedText.replace(
27
- /\[([^\]]+)\](?!\s*\[\/)(?!\s*\])/g, // Match tags that don't have a closing tag after them
28
- (match, key) => {
29
- if (partsMap.has(key)) {
30
- return match; // Keep the reference if we already have content for this key
31
- } else {
32
- console.warn(`Warning: Tag [${key}] has no content and was not previously defined`);
33
- return ''; // Remove invalid tags
34
- }
35
- }
36
- );
37
-
38
- const result: string[] = [];
39
- const parts = processedText
40
- .trim()
41
- .split(/\[([^\]]+)\]/)
42
- .filter(Boolean);
43
-
44
- parts.forEach((part) => {
45
- if (partsMap.has(part)) {
46
- // Add the stored part content
47
- result.push(partsMap.get(part)!);
48
- } else if (part.trim()) {
49
- // Add non-bracketed content
50
- result.push(part.trim());
51
- }
52
- });
53
-
54
- return result;
55
- }
56
-
57
- /**
58
- * Splits text into natural sections based on text structure.
59
- * Works with plain text without requiring markdown or special formatting.
60
- * Empty lines are treated as natural separators between parts.
61
- *
62
- * @param text - The input text to be split into sections
63
- * @param maxLinesPerSlide - Maximum number of lines to include in a single slide (default: 6)
64
- * @returns An array of content sections
65
- */
66
- export const getSlideParts = (text?: string, maxLinesPerSlide: number = 6): string[] => {
67
- if (!text) return [];
68
-
69
- // Process parts in brackets and create a map for the entire text first
70
- const partsMap = new Map<string, string>();
71
-
72
- // Extract all content with closing tags
73
- text.replace(
74
- /\[(.*?)\]([\s\S]*?)\[\/\1\]/g,
75
- (match, key, content) => {
76
- if (!partsMap.has(key)) {
77
- partsMap.set(key, content.trim());
78
- }
79
- return match; // Return original match to not modify the text yet
80
- },
81
- );
82
-
83
- // Now split by empty lines to respect user-defined separations
84
- const paragraphBlocks = text.split(/\n\s*\n/)
85
- .filter(block => block.trim().length > 0)
86
- .map(block => block.trim());
87
-
88
- const result: string[] = [];
89
-
90
- // Process each paragraph block separately
91
- paragraphBlocks.forEach(block => {
92
- // Check if this block is a solo tag reference
93
- const soloTagMatch = block.match(/^\s*\[([^\]]+)\]\s*$/);
94
- if (soloTagMatch && partsMap.has(soloTagMatch[1])) {
95
- // This is a solo tag reference, process the referenced content
96
- const referencedContent = partsMap.get(soloTagMatch[1])!;
97
- processContent(referencedContent, result, maxLinesPerSlide);
98
- } else {
99
- // Process bracketed content within this regular block
100
- const blockParts = getParts(block);
101
- blockParts.forEach(part => {
102
- processContent(part, result, maxLinesPerSlide);
103
- });
104
- }
105
- });
106
-
107
- return result;
108
- }
109
-
110
- /**
111
- * Helper function to process content and add it to the result array
112
- */
113
- function processContent(content: string, result: string[], maxLinesPerSlide: number): void {
114
- // Check if content has line breaks that should be respected
115
- const lines = content.split('\n').filter(line => line.trim().length > 0);
116
-
117
- // If we have multiple lines and more than maxLinesPerSlide, create slides based on line count
118
- if (lines.length > 1) {
119
- if (lines.length > maxLinesPerSlide) {
120
- // Break into multiple slides based on maxLinesPerSlide
121
- let currentSlide: string[] = [];
122
-
123
- lines.forEach(line => {
124
- if (currentSlide.length >= maxLinesPerSlide) {
125
- result.push(currentSlide.join('\n'));
126
- currentSlide = [];
127
- }
128
- currentSlide.push(line);
129
- });
130
-
131
- // Add the final slide if it exists
132
- if (currentSlide.length > 0) {
133
- result.push(currentSlide.join('\n'));
134
- }
135
- } else {
136
- // Just use the whole content as a single slide
137
- result.push(content);
138
- }
139
- } else {
140
- // For single large paragraphs (no line breaks), try to find natural sentence groups
141
- const sentences = content.split(/(?<=[.!?])\s+/)
142
- .filter(s => s.trim().length > 0);
143
-
144
- // Group sentences into reasonable chunks
145
- const sentenceGroups: string[] = [];
146
- let currentGroup: string[] = [];
147
- let currentLength = 0;
148
-
149
- sentences.forEach(sentence => {
150
- // Natural breakpoint based on content and length
151
- // Group 3-5 sentences together or until we reach ~300 chars
152
- if (currentGroup.length >= 4 || currentLength > 250) {
153
- sentenceGroups.push(currentGroup.join(' '));
154
- currentGroup = [];
155
- currentLength = 0;
156
- }
157
-
158
- currentGroup.push(sentence);
159
- currentLength += sentence.length;
160
- });
161
-
162
- // Add the last group if it exists
163
- if (currentGroup.length > 0) {
164
- sentenceGroups.push(currentGroup.join(' '));
165
- }
166
-
167
- // If we created multiple groups, use them
168
- if (sentenceGroups.length > 1) {
169
- result.push(...sentenceGroups);
170
- } else {
171
- // Otherwise just use the whole part
172
- result.push(content);
173
- }
174
- }
175
- }
1
+ export * from './slide.js';
2
+ export * from './lyrics.js';
package/jest.config.js ADDED
@@ -0,0 +1,14 @@
1
+ /** @type {import('ts-jest').JestConfigWithTsJest} */
2
+ export default {
3
+ preset: 'ts-jest',
4
+ testEnvironment: 'node',
5
+ extensionsToTreatAsEsm: ['.ts'],
6
+ moduleNameMapper: {
7
+ '^(\\.{1,2}/.*)\\.js$': '$1',
8
+ },
9
+ transform: {
10
+ '^.+\\.tsx?$': ['ts-jest', {
11
+ useESM: true,
12
+ }],
13
+ },
14
+ };
package/lyrics.test.ts ADDED
@@ -0,0 +1,66 @@
1
+ import { getLyricsParts } from './lyrics';
2
+
3
+ const testLyrics = `[partname 1] (indication 1)
4
+
5
+ content 1
6
+
7
+ [/partname 1]
8
+
9
+ [partname 1] (indication 2)
10
+
11
+ [partname 2]
12
+
13
+ content 2
14
+
15
+ [/partname 2]
16
+
17
+ [interlude 1]
18
+
19
+ [partname 3]
20
+
21
+ content without partname container`;
22
+
23
+ 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
+ });
package/lyrics.ts ADDED
@@ -0,0 +1,81 @@
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
+
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>();
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
+ );
41
+
42
+ // Split the lyrics into lines and process them
43
+ const lines = cleanedText.split('\n');
44
+ let currentPart: LyricPart | null = null;
45
+
46
+ for (let i = 0; i < lines.length; i++) {
47
+ const line = lines[i].trim();
48
+
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);
58
+
59
+ currentPart = {
60
+ name: partName,
61
+ repetition: partCount > 1,
62
+ indication,
63
+ content: partContentMap.get(partName)
64
+ };
65
+ parts.push(currentPart);
66
+ continue;
67
+ }
68
+
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
+ }
78
+ }
79
+
80
+ return parts;
81
+ }
package/package.json CHANGED
@@ -1,20 +1,26 @@
1
1
  {
2
2
  "name": "lyrics-structure",
3
- "version": "1.0.6",
3
+ "version": "1.2.0",
4
+ "description": "Parser for lyrics with structured sections, names, and indications",
4
5
  "main": "dist/index.js",
5
6
  "types": "dist/index.d.ts",
6
7
  "scripts": {
7
- "test": "node dist/test.js",
8
- "build": "tsc"
8
+ "build": "tsc",
9
+ "test": "jest",
10
+ "test:watch": "jest --watch"
9
11
  },
10
- "keywords": [],
12
+ "keywords": [
13
+ "lyrics",
14
+ "parser",
15
+ "slides",
16
+ "sections"
17
+ ],
11
18
  "author": "",
12
- "license": "ISC",
13
- "type": "module",
14
- "description": "",
19
+ "license": "MIT",
15
20
  "devDependencies": {
16
- "@types/node": "^22.13.10",
17
- "ts-node": "^10.9.2",
18
- "typescript": "^5.8.2"
21
+ "@types/jest": "^29.0.0",
22
+ "jest": "^29.0.0",
23
+ "ts-jest": "^29.0.0",
24
+ "typescript": "^5.0.0"
19
25
  }
20
26
  }
package/slide.test.ts ADDED
@@ -0,0 +1,67 @@
1
+ import { getSlideParts } from './slide.js';
2
+
3
+ describe('Comprehensive Tests', () => {
4
+ test('handles all cases in one comprehensive input', () => {
5
+ const comprehensiveInput = `
6
+ [verse]
7
+ First line of verse
8
+ Second line of verse
9
+ Third line of verse
10
+ Fourth line of verse
11
+ Fifth line of verse
12
+ [/verse]
13
+ [verse]
14
+ (inside parentheses)
15
+ Regular text line 1
16
+ Regular text line 2
17
+
18
+ [chorus]
19
+ First chorus line
20
+ Second chorus line
21
+ [/chorus]
22
+
23
+ [verse 1]
24
+ This is a very long line that should be considered too long for the slide
25
+ This is another very long line that should also be considered too long
26
+ Short line 1
27
+ Short line 2
28
+ [/verse 1]
29
+
30
+ Regular line 1
31
+ Regular line 2
32
+
33
+ More content
34
+
35
+ This is a very long line that exceeds forty characters for testing purposes
36
+ This is another very long line that also exceeds the forty character limit
37
+ Another very long line that should be considered too long for the slide
38
+ And yet another very long line that should be split into a new section
39
+ Regular line
40
+
41
+ [verse 2]
42
+ First line with spaces
43
+ Second line with spaces
44
+ [/verse 2]
45
+
46
+ [chorus]`;
47
+
48
+ expect(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
+
67
+ });