Skip to content

Commit d883c9d

Browse files
committed
feat(runtime): 优化歌词片段处理逻辑,显式跳过空格和换行符
- 在 SNRuntime 类中,增强歌词片段的处理逻辑,显式跳过空格和换行符,确保歌词元素的准确性。 - 更新代码注释,符合jsdoc规范,提升可读性和维护性。 - 移除冗余的空格处理逻辑,简化代码结构。
1 parent 811f3dd commit d883c9d

File tree

4 files changed

+317
-9
lines changed

4 files changed

+317
-9
lines changed

lib/src/config/config.spec.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { describe, it, expect, beforeEach } from 'vitest';
2+
import { SNConfig } from './config';
3+
import { SNChordType, SNScoreType } from '@types';
4+
5+
// Mock HTMLDivElement for testing purposes
6+
class MockHTMLDivElement implements Partial<HTMLElement> {
7+
clientWidth: number;
8+
clientHeight: number;
9+
constructor(width: number, height: number) {
10+
this.clientWidth = width;
11+
this.clientHeight = height;
12+
}
13+
}
14+
15+
/**
16+
* SNConfig 配置类测试
17+
* 该模块包含对 SNConfig 类及其方法的测试。
18+
* @module SNConfig
19+
*/
20+
describe('SNConfig 配置类测试', () => {
21+
let mockContainer: MockHTMLDivElement;
22+
23+
beforeEach(() => {
24+
mockContainer = new MockHTMLDivElement(1000, 1200);
25+
});
26+
27+
/**
28+
* 测试 SNConfig 构造函数
29+
* 应该根据提供的选项或默认值正确初始化配置属性。
30+
* @function constructor
31+
*/
32+
it('应该根据提供的选项或默认值正确初始化配置属性', () => {
33+
// 测试默认初始化
34+
new SNConfig(mockContainer as HTMLDivElement);
35+
expect(SNConfig.height).toBe(1200);
36+
expect(SNConfig.content.infoShow).toBe(true);
37+
expect(SNConfig.score.chordHeight).toBe(0);
38+
expect(SNConfig.debug).toEqual({});
39+
40+
// 测试自定义选项初始化
41+
const customOptions = {
42+
width: 800,
43+
height: 600,
44+
content: { infoShow: false, padding: 15 },
45+
info: { height: 100, padding: 5 },
46+
score: { lineHeight: 60, chordType: SNChordType.Guitar },
47+
debug: true,
48+
};
49+
new SNConfig(mockContainer as HTMLDivElement, customOptions);
50+
expect(SNConfig.width).toBe(800);
51+
expect(SNConfig.height).toBe(600);
52+
expect(SNConfig.content.infoShow).toBe(false);
53+
expect(SNConfig.info.height).toBe(100);
54+
expect(SNConfig.score.lineHeight).toBe(60);
55+
expect(SNConfig.score.chordType).toBe(SNChordType.Guitar);
56+
expect(SNConfig.debug).not.toEqual({});
57+
});
58+
59+
/**
60+
* 测试 update 方法
61+
* 应该正确更新配置属性,并处理部分更新和合并。
62+
* @function update
63+
*/
64+
it('应该正确更新配置属性', () => {
65+
new SNConfig(mockContainer as HTMLDivElement);
66+
67+
// 更新部分属性
68+
SNConfig.update({ width: 700, score: { chordHeight: 30 } });
69+
expect(SNConfig.width).toBe(700);
70+
expect(SNConfig.height).toBe(1200); // 未更新的属性保持不变
71+
expect(SNConfig.score.chordHeight).toBe(30);
72+
expect(SNConfig.score.lineHeight).toBe(50); // 其他 score 属性应合并而不是覆盖
73+
74+
// 更新所有属性
75+
const newOptions = {
76+
width: 900,
77+
height: 700,
78+
content: { infoShow: false, padding: 20 },
79+
info: { height: 110, padding: 10 },
80+
score: { scoreType: SNScoreType.Simple },
81+
debug: true,
82+
};
83+
SNConfig.update(newOptions);
84+
expect(SNConfig.width).toBe(900);
85+
expect(SNConfig.height).toBe(700);
86+
expect(SNConfig.content.infoShow).toBe(false);
87+
expect(SNConfig.info.height).toBe(110);
88+
expect(SNConfig.score.scoreType).toBe(SNScoreType.Simple);
89+
expect(SNConfig.debug).not.toEqual({});
90+
});
91+
92+
/**
93+
* 测试 reset 方法
94+
* 应该将配置重置为默认值或提供的初始值。
95+
* @function reset
96+
*/
97+
it('应该将配置重置为默认值或提供的初始值', () => {
98+
// 修改一些属性以进行测试
99+
new SNConfig(mockContainer as HTMLDivElement, {
100+
width: 100,
101+
score: { chordHeight: 100 },
102+
}); // Cast to HTMLDivElement
103+
104+
// 重置为默认值 (无参数调用 reset)
105+
SNConfig.reset();
106+
expect(SNConfig.width).toBe(500); // 默认宽度
107+
expect(SNConfig.height).toBe(800); // 默认高度
108+
expect(SNConfig.score.chordHeight).toBe(0); // 默认 chordHeight
109+
expect(SNConfig.content.infoShow).toBe(true); // 默认 infoShow
110+
111+
// 重置并提供新的初始值
112+
SNConfig.reset({ width: 750, info: { padding: 25, height: 120 } });
113+
expect(SNConfig.width).toBe(750);
114+
expect(SNConfig.info.padding).toBe(25);
115+
expect(SNConfig.height).toBe(800); // 未覆盖的属性仍为默认值
116+
});
117+
});

lib/src/config/runtime.spec.ts

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { describe, it, expect, beforeEach } from 'vitest';
2+
import { SNRuntime } from './runtime';
3+
import { SNDataInfo, SNDataType, SNRuntimeOptions } from '@types';
4+
5+
/**
6+
* SNRuntime 运行时数据管理类测试
7+
* 该模块包含对 SNRuntime 类及其数据处理方法的测试。
8+
* @module SNRuntime
9+
*/
10+
describe('SNRuntime 运行时数据管理类测试', () => {
11+
const defaultInfo: SNDataInfo = {
12+
title: '测试歌曲',
13+
composer: '测试作曲',
14+
lyricist: '测试作词',
15+
time: '4/4',
16+
tempo: '快板',
17+
key: 'C',
18+
beat: '1=C',
19+
};
20+
21+
const defaultScore = '1 2 3 4 | 5 6 7 1.';
22+
const defaultLyric = '我 的 歌';
23+
24+
const defaultOptions: SNRuntimeOptions = {
25+
info: defaultInfo,
26+
score: defaultScore,
27+
lyric: defaultLyric,
28+
parsedScore: [],
29+
splitLyrics: [],
30+
type: SNDataType.TEMPLATE,
31+
};
32+
33+
beforeEach(() => {
34+
SNRuntime.clear();
35+
new SNRuntime(defaultOptions);
36+
});
37+
38+
/**
39+
* 测试 SNRuntime 构造函数
40+
* 应该根据提供的选项正确初始化运行时数据属性。
41+
* @function constructor
42+
*/
43+
it('应该根据提供的选项正确初始化运行时数据属性', () => {
44+
expect(SNRuntime.info).toEqual(defaultInfo);
45+
expect(SNRuntime.score).toBe(defaultScore);
46+
expect(SNRuntime.lyric).toBe(defaultLyric);
47+
expect(SNRuntime.parsedScore).toEqual([]);
48+
expect(SNRuntime.splitLyrics).toEqual([]);
49+
expect(SNRuntime.type).toBe(SNDataType.TEMPLATE);
50+
});
51+
52+
/**
53+
* 测试 getTitle 方法
54+
* 应该返回乐谱的标题。
55+
* @function getTitle
56+
*/
57+
it('应该返回乐谱的标题', () => {
58+
expect(SNRuntime.getTitle()).toBe('测试歌曲');
59+
SNRuntime.info.title = '新标题';
60+
expect(SNRuntime.getTitle()).toBe('新标题');
61+
});
62+
63+
/**
64+
* 测试 clear 方法
65+
* 应该将所有运行时数据属性重置为默认空值。
66+
* @function clear
67+
*/
68+
it('应该将所有运行时数据属性重置为默认空值', () => {
69+
SNRuntime.clear();
70+
expect(SNRuntime.info).toEqual({
71+
title: '',
72+
composer: '',
73+
lyricist: '',
74+
time: '',
75+
tempo: '',
76+
key: undefined,
77+
beat: '',
78+
});
79+
expect(SNRuntime.score).toBe('');
80+
expect(SNRuntime.parsedScore).toEqual([]);
81+
expect(SNRuntime.lyric).toBe('');
82+
expect(SNRuntime.splitLyrics).toEqual([]);
83+
});
84+
85+
/**
86+
* 测试 splitLyric 方法
87+
* 应该正确拆分歌词字符串,支持单行、多行、中英文、标点和括号。
88+
* @function splitLyric
89+
*/
90+
describe('splitLyric 方法', () => {
91+
/**
92+
* 应该正确拆分包含中文、英文单词和标点的单行歌词。
93+
* @test
94+
*/
95+
it('应该正确拆分包含中文、英文单词和标点的单行歌词', () => {
96+
expect(SNRuntime.splitLyric('你好世界,Hello World!')).toEqual([
97+
'你',
98+
'好',
99+
'世',
100+
'界,',
101+
'Hello',
102+
'World!',
103+
]);
104+
expect(SNRuntime.splitLyric('A B C.')).toEqual(['A', 'B', 'C.']);
105+
expect(SNRuntime.splitLyric('中文单词.')).toEqual([
106+
'中',
107+
'文',
108+
'单',
109+
'词.',
110+
]);
111+
});
112+
113+
/**
114+
* 应该正确拆分包含括号内容的歌词。
115+
* @test
116+
*/
117+
it('应该正确拆分包含括号内容的歌词', () => {
118+
expect(SNRuntime.splitLyric('这是一段(括号内容)歌词。')).toEqual([
119+
'这',
120+
'是',
121+
'一',
122+
'段',
123+
'括号内容',
124+
'歌',
125+
'词。',
126+
]);
127+
expect(SNRuntime.splitLyric('Another(example)here')).toEqual([
128+
'Another',
129+
'example',
130+
'here',
131+
]);
132+
});
133+
134+
/**
135+
* 应该正确拆分竖排多行歌词格式。
136+
* @test
137+
*/
138+
it('应该正确拆分竖排多行歌词格式', () => {
139+
const multiLineLyric = '我的[1.第一部分][2.第二部分][3.第三部分]都结束了';
140+
expect(SNRuntime.splitLyric(multiLineLyric)).toEqual([
141+
'我',
142+
'的',
143+
['第', '第', '第'],
144+
['一', '二', '三'],
145+
['部', '部', '部'],
146+
['分', '分', '分'],
147+
'都',
148+
'结',
149+
'束',
150+
'了',
151+
]);
152+
153+
const mixedMultiLine = '前半段[1.上][2.下]后半段';
154+
expect(SNRuntime.splitLyric(mixedMultiLine)).toEqual([
155+
'前',
156+
'半',
157+
'段',
158+
['上', '下'],
159+
'后',
160+
'半',
161+
'段',
162+
]);
163+
});
164+
165+
/**
166+
* 应该处理空字符串和只包含空格的歌词。
167+
* @test
168+
*/
169+
it('应该处理空字符串和只包含空格的歌词', () => {
170+
expect(SNRuntime.splitLyric('')).toEqual([]);
171+
expect(SNRuntime.splitLyric(' ')).toEqual([]);
172+
});
173+
174+
/**
175+
* 应该正确处理连续的英文单词和数字,并跳过内部空格。
176+
* @test
177+
*/
178+
it('应该正确处理连续的英文单词和数字,并跳过内部空格', () => {
179+
expect(SNRuntime.splitLyric('abc def123 ghi')).toEqual([
180+
'abc',
181+
'def123',
182+
'ghi',
183+
]);
184+
expect(SNRuntime.splitLyric('wordAwordB')).toEqual(['wordAwordB']);
185+
});
186+
});
187+
});

lib/src/config/runtime.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ export class SNRuntime {
6767
let i = 0;
6868
while (i < fragment.length) {
6969
const char = fragment[i];
70+
// 显式跳过空格和换行符,它们不应作为独立的歌词元素
71+
if (char === ' ' || char === '\n') {
72+
i++;
73+
continue;
74+
}
75+
7076
// 括号包裹内容整体为一个音
7177
if (openBrackets.includes(char)) {
7278
let j = i + 1;
@@ -89,14 +95,13 @@ export class SNRuntime {
8995
j++;
9096
}
9197
arr.push(word);
92-
i = j - 1;
98+
i = j; // 确保 i 指向单词结束后的下一个字符
99+
continue;
93100
} else if (punctuation.includes(char)) {
94101
// 标点附加到前一个音
95102
if (arr.length > 0) {
96103
arr[arr.length - 1] += char;
97104
}
98-
} else if (char === ' ') {
99-
// 跳过空格(英文分词已处理)
100105
} else {
101106
// 中文或其它单字
102107
arr.push(char);
@@ -124,7 +129,9 @@ export class SNRuntime {
124129
const multiLineRegex = /\[(\d+)\.([^\]]*)\]/y; // sticky flag for position matching
125130
let fragment = '';
126131
while (i < len) {
127-
if (lyric[i] === '[') {
132+
multiLineRegex.lastIndex = i;
133+
const match = multiLineRegex.exec(lyric);
134+
if (match) {
128135
// 先处理前面累积的普通片段
129136
if (fragment) {
130137
const arr = this.splitLyricFragment(fragment);
@@ -175,14 +182,13 @@ export class SNRuntime {
175182
continue;
176183
}
177184
}
178-
// 普通字符,累积到fragment
185+
// 普通字符,累积到fragment (此处不应过滤空格,交给 splitLyricFragment 处理)
179186
const char = lyric[i];
180-
if (char !== ' ' && char !== '\n') {
187+
if (char !== '\n') {
181188
fragment += char;
182189
}
183190
i++;
184191
}
185-
// 处理最后一个片段
186192
if (fragment) {
187193
const arr = this.splitLyricFragment(fragment);
188194
for (const ch of arr) result.push(ch);

lib/src/utils/transition.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,6 @@ export class SNTransition {
277277
octaveMarker = '_'.repeat(Math.abs(note.octaveCount));
278278
}
279279
// 组合生成 SimpleNotation 格式的音高字符串
280-
console.log(`${note.upDownCount ? '#' : ''}${note.note}${octaveMarker}`);
281-
282280
return `${note.upDownCount ? '#' : ''}${note.note}${octaveMarker}`;
283281
},
284282

0 commit comments

Comments
 (0)