@@ -2,168 +2,162 @@ import { describe, it, expect } from "vitest";
22import { highlightRA } from "./raHighlight" ;
33import { renderRAPreview } from "./RAPreview" ;
44
5- describe ( "RA highlighter" , ( ) => {
6- it ( "should highlight σ operator in purple" , ( ) => {
7- const html = highlightRA ( "σ[age > 20](Person)" ) ;
8- expect ( html ) . toContain ( "color: #7c3aed" ) ; // operator color
9- expect ( html ) . toContain ( "σ" ) ;
5+ /** Strip HTML tags and decode entities to recover visible text */
6+ function visibleText ( html : string ) : string {
7+ return html
8+ . replace ( / < [ ^ > ] + > / g, "" )
9+ . replace ( / & l t ; / g, "<" ) . replace ( / & g t ; / g, ">" )
10+ . replace ( / & a m p ; / g, "&" ) . replace ( / & q u o t ; / g, '"' ) ;
11+ }
12+
13+ // ─── Character count invariant ──────────────────────────────────────────────
14+ // The editor overlay MUST produce visible text identical to the input.
15+ // These tests are the most important — if any fail, the editor is broken.
16+
17+ describe ( "RA highlighter character preservation" , ( ) => {
18+ const inputs = [
19+ "Person" ,
20+ "σ[age > 20](Person)" ,
21+ "π[name, city](Person)" ,
22+ "ρ[name→fullName](Person)" ,
23+ "σ_{age > 20}(Person)" ,
24+ "σ{age > 20}(Person)" ,
25+ "PI [name, city](Person)" ,
26+ "PI [name, person_id, address, postal_code] SIGMA [city='York'] Person" ,
27+ "PI name, person_id SIGMA city='York' Person" ,
28+ "PI _name, person_id Person" ,
29+ "A <- σ[age > 20](Person)\nπ[name](A)" ,
30+ "-- comment\nPerson" ,
31+ "Person ⋈ Student" ,
32+ "Person |X| Student" ,
33+ "Person |><| Student" ,
34+ "ρ[name->fullName](Person)" ,
35+ "σ[a <> 1](T)" ,
36+ "σ[a != 1](T)" ,
37+ "σ[a >= 1](T)" ,
38+ "σ[a <= 1](T)" ,
39+ "γ[city; COUNT(id) AS cnt](Person)" ,
40+ "Person ⋈[Person.id = Student.id] Student" ,
41+ "σ[name = 'Alice'](Person)" ,
42+ "PI [name SIGMA [age > 20] Person" , // unclosed bracket
43+ "σ age > 20 (Person)" , // implicit subscript
44+ "" ,
45+ ] ;
46+
47+ for ( const input of inputs ) {
48+ it ( `preserves: ${ JSON . stringify ( input ) . slice ( 0 , 60 ) } ` , ( ) => {
49+ if ( input === "" ) return ; // empty input = empty output
50+ const html = highlightRA ( input ) ;
51+ expect ( visibleText ( html ) ) . toBe ( input ) ;
52+ } ) ;
53+ }
54+ } ) ;
55+
56+ // ─── Token coloring ─────────────────────────────────────────────────────────
57+
58+ describe ( "RA highlighter coloring" , ( ) => {
59+ it ( "colors σ operator" , ( ) => {
60+ expect ( highlightRA ( "σ[age > 20](Person)" ) ) . toContain ( "color: #7c3aed" ) ;
1061 } ) ;
1162
12- it ( "should render brackets faintly" , ( ) => {
13- const html = highlightRA ( "σ[age > 20](Person)" ) ;
14- expect ( html ) . toContain ( "opacity: 0.5" ) ; // faint brackets
63+ it ( "colors keyword operators" , ( ) => {
64+ const html = highlightRA ( "sigma[age > 20](Person)" ) ;
65+ expect ( html ) . toContain ( "color: #7c3aed" ) ;
66+ expect ( html ) . toContain ( "sigma" ) ;
67+ } ) ;
68+
69+ it ( "colors binary keyword operators" , ( ) => {
70+ expect ( highlightRA ( "A cross B" ) ) . toContain ( "color: #7c3aed" ) ;
1571 } ) ;
1672
17- it ( "should render subscript content in light purple" , ( ) => {
18- const html = highlightRA ( "π[name](Person)" ) ;
19- expect ( html ) . toContain ( "color: #c084fc" ) ; // subscript styling
73+ it ( "renders brackets faintly" , ( ) => {
74+ expect ( highlightRA ( "σ[age > 20](Person)" ) ) . toContain ( "opacity: 0.5" ) ;
2075 } ) ;
2176
22- it ( "should highlight string literals in green" , ( ) => {
77+ it ( "colors string literals green" , ( ) => {
2378 const html = highlightRA ( "σ[name = 'Alice'](Person)" ) ;
24- expect ( html ) . toContain ( "color: #059669" ) ; // string color
79+ expect ( html ) . toContain ( "color: #059669" ) ;
2580 expect ( html ) . toContain ( "Alice" ) ;
2681 } ) ;
2782
28- it ( "should highlight numbers in amber" , ( ) => {
29- const html = highlightRA ( "σ[age > 20](Person)" ) ;
30- expect ( html ) . toContain ( "color: #d97706" ) ; // number color
83+ it ( "colors numbers amber" , ( ) => {
84+ expect ( highlightRA ( "σ[age > 20](Person)" ) ) . toContain ( "color: #d97706" ) ;
3185 } ) ;
3286
33- it ( "should highlight AND/OR in blue" , ( ) => {
87+ it ( "colors AND/OR/NOT blue" , ( ) => {
3488 const html = highlightRA ( "σ[a > 1 and b < 2](T)" ) ;
35- expect ( html ) . toContain ( "color: #2563eb" ) ; // logic color
36- expect ( html ) . toContain ( "and" ) ;
89+ expect ( html ) . toContain ( "color: #2563eb" ) ;
3790 } ) ;
3891
39- it ( "should highlight comments in gray italic" , ( ) => {
92+ it ( "colors comments gray italic" , ( ) => {
4093 const html = highlightRA ( "-- this is a comment\nPerson" ) ;
4194 expect ( html ) . toContain ( "font-style: italic" ) ;
42- expect ( html ) . toContain ( "this is a comment" ) ;
4395 } ) ;
4496
45- it ( "should render <- with assignment styling " , ( ) => {
97+ it ( "colors <- assignment" , ( ) => {
4698 const html = highlightRA ( "A <- Person" ) ;
47- expect ( html ) . toContain ( "<-" ) ;
4899 expect ( html ) . toContain ( "color: #7c3aed" ) ;
100+ expect ( html ) . toContain ( "<-" ) ;
49101 } ) ;
50102
51- it ( "should render -> with operator styling " , ( ) => {
103+ it ( "colors -> rename arrow " , ( ) => {
52104 const html = highlightRA ( "ρ[name->fullName](Person)" ) ;
53105 expect ( html ) . toContain ( "->" ) ;
54106 } ) ;
55107
56- it ( "should handle LaTeX-style _{} notation " , ( ) => {
108+ it ( "colors _ as bracket when before { " , ( ) => {
57109 const html = highlightRA ( "σ_{age > 20}(Person)" ) ;
58- expect ( html ) . toContain ( "opacity: 0.5" ) ; // faint brackets
59- expect ( html ) . toContain ( "color: #c084fc" ) ; // subscript content
60- } ) ;
61-
62- it ( "should highlight keyword operators" , ( ) => {
63- const html = highlightRA ( "sigma[age > 20](Person)" ) ;
64- expect ( html ) . toContain ( "color: #7c3aed" ) ; // operator color
65- expect ( html ) . toContain ( "sigma" ) ;
66- } ) ;
67-
68- it ( "should highlight binary keyword operators" , ( ) => {
69- const html = highlightRA ( "A cross B" ) ;
70- expect ( html ) . toContain ( "color: #7c3aed" ) ;
71- expect ( html ) . toContain ( "cross" ) ;
72- } ) ;
73-
74- it ( "should preserve newlines" , ( ) => {
75- const html = highlightRA ( "A <- Person\nA" ) ;
76- expect ( html ) . toContain ( "\n" ) ;
77- } ) ;
78-
79- it ( "should handle implicit subscripts with whitespace" , ( ) => {
80- const html = highlightRA ( "σ age > 20 (Person)" ) ;
81- expect ( html ) . toContain ( "color: #c084fc" ) ; // subscript styling for implicit content
82- } ) ;
83-
84- it ( "should preserve character count for _{} notation" , ( ) => {
85- const input = "σ_{age > 20}(Person)" ;
86- const html = highlightRA ( input ) ;
87- // Strip HTML tags and decode entities to get visible text
88- const textOnly = html
89- . replace ( / < [ ^ > ] + > / g, "" )
90- . replace ( / & l t ; / g, "<" ) . replace ( / & g t ; / g, ">" )
91- . replace ( / & a m p ; / g, "&" ) . replace ( / & q u o t ; / g, '"' ) ;
92- expect ( textOnly . length ) . toBe ( input . length ) ;
93- expect ( textOnly ) . toBe ( input ) ;
110+ expect ( html ) . toContain ( "opacity: 0.5" ) ;
94111 } ) ;
95112
96- it ( "should preserve character count for keyword _{} notation" , ( ) => {
97- const input = "sigma_{age > 20}(Person)" ;
98- const html = highlightRA ( input ) ;
99- const textOnly = html
100- . replace ( / < [ ^ > ] + > / g, "" )
101- . replace ( / & l t ; / g, "<" ) . replace ( / & g t ; / g, ">" )
102- . replace ( / & a m p ; / g, "&" ) . replace ( / & q u o t ; / g, '"' ) ;
103- expect ( textOnly . length ) . toBe ( input . length ) ;
104- expect ( textOnly ) . toBe ( input ) ;
113+ it ( "does not color _ when part of identifier" , ( ) => {
114+ const html = highlightRA ( "PI _name (Person)" ) ;
115+ // _name should not have bracket styling
116+ expect ( html ) . not . toMatch ( / o p a c i t y .* _ n / ) ;
105117 } ) ;
106118
107- it ( "should preserve character count for [] notation" , ( ) => {
108- const input = "σ[age > 20](Person)" ;
109- const html = highlightRA ( input ) ;
110- const textOnly = html
111- . replace ( / < [ ^ > ] + > / g, "" )
112- . replace ( / & l t ; / g, "<" ) . replace ( / & g t ; / g, ">" )
113- . replace ( / & a m p ; / g, "&" ) . replace ( / & q u o t ; / g, '"' ) ;
114- expect ( textOnly . length ) . toBe ( input . length ) ;
115- } ) ;
116-
117- it ( "should preserve character count for {} notation" , ( ) => {
118- const input = "σ{age > 20}(Person)" ;
119- const html = highlightRA ( input ) ;
120- const textOnly = html
121- . replace ( / < [ ^ > ] + > / g, "" )
122- . replace ( / & l t ; / g, "<" ) . replace ( / & g t ; / g, ">" )
123- . replace ( / & a m p ; / g, "&" ) . replace ( / & q u o t ; / g, '"' ) ;
124- expect ( textOnly . length ) . toBe ( input . length ) ;
119+ it ( "preserves newlines" , ( ) => {
120+ expect ( highlightRA ( "A <- Person\nA" ) ) . toContain ( "\n" ) ;
125121 } ) ;
126122} ) ;
127123
124+ // ─── RAPreview rendering ────────────────────────────────────────────────────
125+
128126describe ( "RA preview" , ( ) => {
129127 it ( "should render paren-free implicit projection without eating the table name" , ( ) => {
130128 const html = renderRAPreview ( "PI person_id, address Person" ) ;
131- // "Person" must NOT be inside a <sub> tag — it's the operand, not part of the subscript
132- expect ( html ) . toMatch ( / P e r s o n (? ! .* < \/ s u b > ) / ) ;
133- // The subscript should contain the column list
134- expect ( html ) . toContain ( "<sub" ) ;
135129 expect ( html ) . toMatch ( / < s u b [ ^ > ] * > .* p e r s o n _ i d .* a d d r e s s .* < \/ s u b > / ) ;
130+ // Person must not be in a subscript
131+ expect ( html ) . not . toMatch ( / < s u b [ ^ > ] * > .* P e r s o n .* < \/ s u b > / s) ;
136132 } ) ;
137133
138134 it ( "should render paren-free implicit projection with Unicode symbol" , ( ) => {
139135 const html = renderRAPreview ( "π person_id, address Person" ) ;
140- expect ( html ) . toMatch ( / P e r s o n (? ! .* < \/ s u b > ) / ) ;
141136 expect ( html ) . toMatch ( / < s u b [ ^ > ] * > .* p e r s o n _ i d .* < \/ s u b > / ) ;
142137 } ) ;
143138
144139 it ( "should still render parenthesised implicit subscript correctly" , ( ) => {
145140 const html = renderRAPreview ( "π person_id, address (Person)" ) ;
146- // All of "person_id, address" should be in subscript
147141 expect ( html ) . toMatch ( / < s u b [ ^ > ] * > .* p e r s o n _ i d .* a d d r e s s .* < \/ s u b > / ) ;
148142 } ) ;
149143
150144 it ( "should render paren-free σ condition Table correctly" , ( ) => {
151145 const html = renderRAPreview ( "sigma age > 20 Person" ) ;
152- // "Person" should not be in the subscript
153146 expect ( html ) . toContain ( "Person" ) ;
154147 expect ( html ) . toMatch ( / < s u b [ ^ > ] * > .* a g e .* < \/ s u b > / ) ;
155148 } ) ;
156149
150+ it ( "should not swallow content after unclosed bracket" , ( ) => {
151+ const html = renderRAPreview ( "PI [name, person_id SIGMA city='York' Person" ) ;
152+ const opMatches = html . match ( / r a - p r e v - o p / g) ;
153+ expect ( opMatches ! . length ) . toBeGreaterThanOrEqual ( 2 ) ;
154+ } ) ;
155+
157156 it ( "should render chained paren-free operators correctly" , ( ) => {
158157 const html = renderRAPreview ( "PI name, person_id, address, postal_code SIGMA city='York' Person" ) ;
159- // SIGMA should be rendered as its own operator, not swallowed into PI's subscript
160- // Count operator spans — should have both π and σ
161158 const opMatches = html . match ( / r a - p r e v - o p / g) ;
162- expect ( opMatches ! . length ) . toBeGreaterThanOrEqual ( 2 ) ; // at least π and σ
163- // PI's subscript should contain the column list only
159+ expect ( opMatches ! . length ) . toBeGreaterThanOrEqual ( 2 ) ;
164160 expect ( html ) . toMatch ( / < s u b [ ^ > ] * > .* n a m e .* p o s t a l _ c o d e .* < \/ s u b > / ) ;
165- // "Person" should not be inside a subscript of PI
166- expect ( html ) . not . toMatch ( / < s u b [ ^ > ] * > .* P e r s o n .* < \/ s u b > / s) ;
167161 } ) ;
168162
169163 it ( "should render chained Unicode operators correctly" , ( ) => {
@@ -174,31 +168,7 @@ describe("RA preview", () => {
174168
175169 it ( "should not treat underscore-prefixed column names as LaTeX subscript" , ( ) => {
176170 const html = renderRAPreview ( "PI _name, person_id Person" ) ;
177- // _name should not be split — the underscore is part of the column name
178171 expect ( html ) . toMatch ( / < s u b [ ^ > ] * > .* _ n a m e .* p e r s o n _ i d .* < \/ s u b > / ) ;
179- expect ( html ) . not . toMatch ( / o p a c i t y / ) ; // no faint bracket styling for _
180- } ) ;
181- } ) ;
182-
183- describe ( "RA highlighter character preservation with underscore columns" , ( ) => {
184- it ( "should preserve character count with underscore-prefixed columns" , ( ) => {
185- const input = "PI _name, person_id (Person)" ;
186- const html = highlightRA ( input ) ;
187- const textOnly = html
188- . replace ( / < [ ^ > ] + > / g, "" )
189- . replace ( / & l t ; / g, "<" ) . replace ( / & g t ; / g, ">" )
190- . replace ( / & a m p ; / g, "&" ) . replace ( / & q u o t ; / g, '"' ) ;
191- expect ( textOnly ) . toBe ( input ) ;
192- } ) ;
193-
194- it ( "should still handle real _{} LaTeX notation with Unicode symbol" , ( ) => {
195- const input = "σ_{age > 20}(Person)" ;
196- const html = highlightRA ( input ) ;
197- const textOnly = html
198- . replace ( / < [ ^ > ] + > / g, "" )
199- . replace ( / & l t ; / g, "<" ) . replace ( / & g t ; / g, ">" )
200- . replace ( / & a m p ; / g, "&" ) . replace ( / & q u o t ; / g, '"' ) ;
201- expect ( textOnly ) . toBe ( input ) ;
202- expect ( html ) . toContain ( "opacity: 0.5" ) ; // _ rendered as faint bracket
172+ expect ( html ) . not . toMatch ( / o p a c i t y / ) ;
203173 } ) ;
204174} ) ;
0 commit comments