Skip to content

Commit 96fbc8c

Browse files
committed
Rewrite the parser for web engine
1 parent 81af50e commit 96fbc8c

File tree

4 files changed

+420
-196
lines changed

4 files changed

+420
-196
lines changed

websur/src/artifact-component.tsx

Lines changed: 70 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
33
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
44
import { Button } from '@/components/ui/button';
55
import { Download, FileText } from 'lucide-react';
6-
import { SurParser, SurDocument, Note, Beat, Element, ElementType, NotePitch } from './lib/sur-parser';
6+
import { SurParser, Note, Beat, Element, ElementType, NotePitch } from './lib/sur-parser';
7+
import type { SurDocument, Section } from './lib/sur-parser/types';
78
import html2pdf from 'html2pdf.js';
9+
import { SurFormatter } from './lib/sur-parser/formatter';
810

911
const DEFAULT_SUR = `%% CONFIG
1012
name: "Albela Sajan"
@@ -92,31 +94,33 @@ const renderElement = (element: Element): string => {
9294
}
9395

9496
// Return whichever is present
95-
return lyricsStr || noteStr;
97+
return lyricsStr || noteStr || '-';
9698
};
9799

98-
const renderBeat = (beat: Beat): string => {
99-
if (!beat || !beat.elements) {
100+
const renderBeat = (beat: Beat | number): string => {
101+
if (typeof beat === 'number') {
102+
return beat.toString();
103+
}
104+
105+
if (!beat || !beat.elements || beat.elements.length === 0) {
100106
return '-';
101107
}
102108

103109
const elementStrings = beat.elements.map(renderElement);
104110

105-
// Check if any element has lyrics
106-
const hasLyrics = beat.elements.some(e => e.lyrics);
107-
108-
if (hasLyrics) {
109-
// If has lyrics, add spaces between elements and wrap in brackets
111+
// Always wrap in brackets if it's marked as bracketed
112+
if (beat.bracketed) {
110113
return `[${elementStrings.join(' ')}]`;
111-
} else {
112-
// If only notes, join without spaces
113-
return elementStrings.join('');
114114
}
115+
116+
// Join without spaces for pure notes
117+
return elementStrings.join('');
115118
};
116119

120+
// Update the BeatGrid component to handle the new structure
117121
const BeatGrid: React.FC<BeatGridProps> = ({ beats = [], totalBeats = 16, groupSize = 4 }) => {
118-
const beatsToRender = Array.isArray(beats) ? beats : [];
119-
const groups = [];
122+
// Ensure beats is always an array
123+
const beatsToRender = [...beats];
120124

121125
// Fill with empty beats if needed
122126
while (beatsToRender.length < totalBeats) {
@@ -129,6 +133,7 @@ const BeatGrid: React.FC<BeatGridProps> = ({ beats = [], totalBeats = 16, groupS
129133
}
130134

131135
// Group beats
136+
const groups = [];
132137
for (let i = 0; i < totalBeats; i += groupSize) {
133138
const group = beatsToRender.slice(i, i + groupSize);
134139
groups.push(group);
@@ -141,7 +146,8 @@ const BeatGrid: React.FC<BeatGridProps> = ({ beats = [], totalBeats = 16, groupS
141146
<div className="grid grid-cols-4">
142147
{group.map((beat, beatIndex) => {
143148
const renderedBeat = renderBeat(beat);
144-
const isLyrics = beat?.elements?.some(e => e?.lyrics) || false;
149+
const isLyrics = typeof beat !== 'number' &&
150+
beat?.elements?.some(e => e?.lyrics) || false;
145151
const className = isLyrics ? 'text-blue-600 font-medium' : 'text-black';
146152

147153
return (
@@ -314,6 +320,16 @@ const PDFExporter: React.FC<{
314320
);
315321
};
316322

323+
// Add this helper function at the top level
324+
const groupBeatsIntoLines = (beats: Beat[], beatsPerLine: number = 16): Beat[][] => {
325+
const lines: Beat[][] = [];
326+
for (let i = 0; i < beats.length; i += beatsPerLine) {
327+
lines.push(beats.slice(i, i + beatsPerLine));
328+
}
329+
return lines;
330+
};
331+
332+
// Update the SUREditor component
317333
const SUREditor: React.FC<{ content: string; onChange: (content: string) => void }> = ({ content, onChange }) => {
318334
const [editableContent, setEditableContent] = useState(content);
319335
const parser = new SurParser();
@@ -328,16 +344,19 @@ const SUREditor: React.FC<{ content: string; onChange: (content: string) => void
328344
let previewContent = '';
329345
try {
330346
const surDoc = parser.parse(editableContent);
347+
const formatter = new SurFormatter();
348+
331349
if (surDoc.composition.sections.length > 0) {
332-
// Build preview section by section
333350
previewContent = surDoc.composition.sections.map(section => {
334-
// Add section header
335351
const sectionLines = [`#${section.title}`];
336352

337-
// Add each line of beats
338-
section.beats.forEach(beatLine => {
339-
const renderedBeats = beatLine.map(beat => renderBeat(beat)).join(' ');
340-
sectionLines.push(`b: ${renderedBeats}`);
353+
// Group beats into lines
354+
const beatLines = groupBeatsIntoLines(section.beats);
355+
356+
beatLines.forEach((beatLine) => {
357+
// Use the formatter to format the line
358+
const renderedLine = formatter.formatLine(beatLine);
359+
sectionLines.push(`b: ${renderedLine}`);
341360
});
342361

343362
return sectionLines.join('\n');
@@ -417,23 +436,37 @@ const SUREditorViewer = () => {
417436
{(() => {
418437
try {
419438
const surDoc = parseSURFile(content);
420-
return surDoc.composition.sections.map((section, sectionIdx) => (
421-
<div key={sectionIdx} className="space-y-1.5">
422-
<h3 className="text-lg font-semibold text-blue-600 mb-1">
423-
{section.title}
424-
</h3>
425-
<div className="font-mono text-sm space-y-2">
426-
{section.beats.map((beatLine, lineIdx) => (
427-
<BeatGrid
428-
key={`${sectionIdx}-${lineIdx}`}
429-
beats={beatLine}
430-
totalBeats={16}
431-
groupSize={4}
432-
/>
433-
))}
439+
console.log('Rendering document:', surDoc);
440+
441+
return surDoc.composition.sections.map((section, sectionIdx) => {
442+
console.log('Rendering section:', section.title, 'beats:', section.beats);
443+
444+
if (!Array.isArray(section.beats)) {
445+
console.error('Section beats is not an array:', section.beats);
446+
return null;
447+
}
448+
449+
// Group beats into lines
450+
const beatLines = groupBeatsIntoLines(section.beats);
451+
452+
return (
453+
<div key={sectionIdx} className="space-y-1.5">
454+
<h3 className="text-lg font-semibold text-blue-600 mb-1">
455+
{section.title}
456+
</h3>
457+
<div className="font-mono text-sm space-y-2">
458+
{beatLines.map((beatLine, lineIdx) => (
459+
<BeatGrid
460+
key={`${sectionIdx}-${lineIdx}`}
461+
beats={beatLine}
462+
totalBeats={16}
463+
groupSize={4}
464+
/>
465+
))}
466+
</div>
434467
</div>
435-
</div>
436-
));
468+
);
469+
});
437470
} catch (e) {
438471
console.error('Error parsing SUR file:', e);
439472
return <div>Error parsing SUR file</div>;
Lines changed: 85 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,118 @@
1-
import { Beat, Element, Note } from './types';
1+
import { Beat, Element, Note, NotePitch } from './types';
22

33
export class SurFormatter {
4-
private formatLyrics(lyrics: string): string {
5-
// Always wrap lyrics containing spaces in quotes
6-
return lyrics.includes(' ') ? `"${lyrics}"` : lyrics;
4+
private formatNote(note: Note): string {
5+
if (!note) return '';
6+
7+
// Handle special notes
8+
if (note.pitch === NotePitch.SILENCE) return '-';
9+
if (note.pitch === NotePitch.SUSTAIN) return '*';
10+
11+
// Format the note with octave markers
12+
let noteStr = note.pitch.toString();
13+
14+
// Add octave markers
15+
if (note.octave === 1) {
16+
noteStr += "'"; // Upper octave
17+
} else if (note.octave === -1) {
18+
noteStr = '.' + noteStr; // Lower octave
19+
}
20+
21+
return noteStr;
722
}
823

9-
private formatNote(note: Note): string {
10-
const pitch = note.pitch;
11-
if (!note.octave) return pitch;
24+
private formatLyrics(lyrics: string): string {
25+
if (!lyrics) return '';
1226

13-
// Upper octave: add ' after note
14-
if (note.octave === 1) return `${pitch}'`;
15-
// Lower octave: add . before note
16-
if (note.octave === -1) return `.${pitch}`;
27+
// If lyrics contains whitespace or special characters, wrap in quotes
28+
if (lyrics.includes(' ') || /[[\]:*-]/.test(lyrics)) {
29+
return `"${lyrics}"`;
30+
}
1731

18-
return pitch;
32+
return lyrics;
1933
}
2034

2135
private formatElement(element: Element): string {
36+
if (!element) return '';
37+
38+
// Case 1: Both lyrics and note
2239
if (element.lyrics && element.note) {
23-
// Format as lyrics:note, wrapping lyrics in quotes if it contains spaces
2440
return `${this.formatLyrics(element.lyrics)}:${this.formatNote(element.note)}`;
2541
}
42+
43+
// Case 2: Only lyrics
2644
if (element.lyrics) {
27-
// Wrap lyrics in quotes if it contains spaces
2845
return this.formatLyrics(element.lyrics);
2946
}
47+
48+
// Case 3: Only note
3049
if (element.note) {
3150
return this.formatNote(element.note);
3251
}
33-
return '';
52+
53+
return '-'; // Default to silence for empty elements
54+
}
55+
56+
private shouldBracket(beat: Beat): boolean {
57+
// Don't use beat.bracketed as it's only for parsing
58+
59+
// If there's only one element with both lyrics and note, no brackets needed
60+
if (beat.elements.length === 1 && beat.elements[0].lyrics && beat.elements[0].note) {
61+
return false;
62+
}
63+
64+
// If there are multiple elements and any has lyrics, we need brackets
65+
if (beat.elements.length > 1 && beat.elements.some(e => e.lyrics)) {
66+
return true;
67+
}
68+
69+
// If there are multiple elements with spaces needed between them
70+
if (beat.elements.length > 1 && this.needsSpaceBetweenElements(beat.elements)) {
71+
return true;
72+
}
73+
74+
return false;
3475
}
3576

36-
private hasLyrics(elements: Element[]): boolean {
37-
return elements.some(e => e.lyrics !== undefined);
77+
private needsSpaceBetweenElements(elements: Element[]): boolean {
78+
// Need spaces if:
79+
// 1. Any element has lyrics
80+
// 2. Any element has octave markers that could be ambiguous
81+
// 3. Any element needs special formatting
82+
return elements.some(e =>
83+
e.lyrics ||
84+
(e.note && e.note.octave !== 0) ||
85+
(e.note && [NotePitch.SILENCE, NotePitch.SUSTAIN].includes(e.note.pitch))
86+
);
3887
}
3988

4089
private formatBeat(beat: Beat): string {
41-
const elements = beat.elements;
42-
if (elements.length === 0) return '';
43-
if (elements.length === 1) return this.formatElement(elements[0]);
90+
if (!beat || !beat.elements || beat.elements.length === 0) {
91+
return '-';
92+
}
4493

45-
// Multiple elements
46-
const formattedElements = elements.map(e => this.formatElement(e));
94+
const formattedElements = beat.elements.map(e => this.formatElement(e));
4795

48-
// If any element has lyrics, wrap in brackets and separate with spaces
49-
if (this.hasLyrics(elements)) {
96+
// Determine if we need brackets
97+
if (this.shouldBracket(beat)) {
98+
// For beats with lyrics, join all notes without spaces
99+
if (beat.elements.some(e => e.lyrics)) {
100+
const lyricsElement = formattedElements.find(e => e.includes(':') || !e.match(/^[SRGMPDN'.-]*$/));
101+
const noteElements = formattedElements.filter(e => !e.includes(':') && e.match(/^[SRGMPDN'.-]*$/));
102+
const notes = noteElements.join('');
103+
return `[${lyricsElement} ${notes}]`;
104+
}
105+
106+
// For other cases that need brackets
50107
return `[${formattedElements.join(' ')}]`;
51108
}
52109

53-
// Only notes - render side by side without spaces or brackets
110+
// For simple notes without lyrics, join without spaces
54111
return formattedElements.join('');
55112
}
56113

57-
formatBeats(beats: Beat[]): string {
58-
// Always add spaces between beats
59-
return beats
60-
.map(beat => this.formatBeat(beat))
61-
.filter(str => str.length > 0)
62-
.join(' '); // This ensures space between each beat
114+
public formatLine(beats: Beat[]): string {
115+
// Join beats with spaces
116+
return beats.map(beat => this.formatBeat(beat)).join(' ');
63117
}
64118
}

0 commit comments

Comments
 (0)