1+ /*---------------------------------------------------------------------------------------------
2+ * Copyright (c) Microsoft Corporation. All rights reserved.
3+ * Licensed under the MIT License. See License.txt in the project root for license information.
4+ *--------------------------------------------------------------------------------------------*/
5+
6+ import { Range , Position } from 'vscode-languageserver' ;
7+ import { TextDocument } from 'vscode-languageserver-textdocument' ;
8+
9+
10+
11+ export interface LanguageRange extends Range {
12+ languageId : string | undefined ;
13+ attributeValue ?: boolean ;
14+ }
15+
16+ export interface HTMLDocumentRegions {
17+ getEmbeddedDocument ( languageId : string , ignoreAttributeValues ?: boolean ) : TextDocument ;
18+ getLanguageRanges ( range : Range ) : LanguageRange [ ] ;
19+ getLanguageAtPosition ( position : Position ) : string | undefined ;
20+ getLanguagesInDocument ( ) : string [ ] ;
21+ getImportedScripts ( ) : string [ ] ;
22+ }
23+
24+ export const CSS_STYLE_RULE = '__' ;
25+
26+ interface EmbeddedRegion { languageId : string | undefined ; start : number ; end : number ; attributeValue ?: boolean ; }
27+
28+
29+ export function getDocumentRegions ( languageService : LanguageService , document : TextDocument ) : HTMLDocumentRegions {
30+ const regions : EmbeddedRegion [ ] = [ ] ;
31+ const scanner = languageService . createScanner ( document . getText ( ) ) ;
32+ let lastTagName = '' ;
33+ let lastAttributeName : string | null = null ;
34+ let languageIdFromType : string | undefined = undefined ;
35+ const importedScripts : string [ ] = [ ] ;
36+
37+ let token = scanner . scan ( ) ;
38+ while ( token !== TokenType . EOS ) {
39+ switch ( token ) {
40+ case TokenType . StartTag :
41+ lastTagName = scanner . getTokenText ( ) ;
42+ lastAttributeName = null ;
43+ languageIdFromType = 'javascript' ;
44+ break ;
45+ case TokenType . Styles :
46+ regions . push ( { languageId : 'css' , start : scanner . getTokenOffset ( ) , end : scanner . getTokenEnd ( ) } ) ;
47+ break ;
48+ case TokenType . Script :
49+ regions . push ( { languageId : languageIdFromType , start : scanner . getTokenOffset ( ) , end : scanner . getTokenEnd ( ) } ) ;
50+ break ;
51+ case TokenType . AttributeName :
52+ lastAttributeName = scanner . getTokenText ( ) ;
53+ break ;
54+ case TokenType . AttributeValue :
55+ if ( lastAttributeName === 'src' && lastTagName . toLowerCase ( ) === 'script' ) {
56+ let value = scanner . getTokenText ( ) ;
57+ if ( value [ 0 ] === '\'' || value [ 0 ] === '"' ) {
58+ value = value . substr ( 1 , value . length - 1 ) ;
59+ }
60+ importedScripts . push ( value ) ;
61+ } else if ( lastAttributeName === 'type' && lastTagName . toLowerCase ( ) === 'script' ) {
62+ if ( / [ " ' ] ( m o d u l e | ( t e x t | a p p l i c a t i o n ) \/ ( j a v a | e c m a ) s c r i p t | t e x t \/ b a b e l ) [ " ' ] / . test ( scanner . getTokenText ( ) ) ) {
63+ languageIdFromType = 'javascript' ;
64+ } else if ( / [ " ' ] t e x t \/ t y p e s c r i p t [ " ' ] / . test ( scanner . getTokenText ( ) ) ) {
65+ languageIdFromType = 'typescript' ;
66+ } else {
67+ languageIdFromType = undefined ;
68+ }
69+ } else {
70+ const attributeLanguageId = getAttributeLanguage ( lastAttributeName ! ) ;
71+ if ( attributeLanguageId ) {
72+ let start = scanner . getTokenOffset ( ) ;
73+ let end = scanner . getTokenEnd ( ) ;
74+ const firstChar = document . getText ( ) [ start ] ;
75+ if ( firstChar === '\'' || firstChar === '"' ) {
76+ start ++ ;
77+ end -- ;
78+ }
79+ regions . push ( { languageId : attributeLanguageId , start, end, attributeValue : true } ) ;
80+ }
81+ }
82+ lastAttributeName = null ;
83+ break ;
84+ }
85+ token = scanner . scan ( ) ;
86+ }
87+ return {
88+ getLanguageRanges : ( range : Range ) => getLanguageRanges ( document , regions , range ) ,
89+ getEmbeddedDocument : ( languageId : string , ignoreAttributeValues : boolean ) => getEmbeddedDocument ( document , regions , languageId , ignoreAttributeValues ) ,
90+ getLanguageAtPosition : ( position : Position ) => getLanguageAtPosition ( document , regions , position ) ,
91+ getLanguagesInDocument : ( ) => getLanguagesInDocument ( document , regions ) ,
92+ getImportedScripts : ( ) => importedScripts
93+ } ;
94+ }
95+
96+
97+ function getLanguageRanges ( document : TextDocument , regions : EmbeddedRegion [ ] , range : Range ) : LanguageRange [ ] {
98+ const result : LanguageRange [ ] = [ ] ;
99+ let currentPos = range ? range . start : Position . create ( 0 , 0 ) ;
100+ let currentOffset = range ? document . offsetAt ( range . start ) : 0 ;
101+ const endOffset = range ? document . offsetAt ( range . end ) : document . getText ( ) . length ;
102+ for ( const region of regions ) {
103+ if ( region . end > currentOffset && region . start < endOffset ) {
104+ const start = Math . max ( region . start , currentOffset ) ;
105+ const startPos = document . positionAt ( start ) ;
106+ if ( currentOffset < region . start ) {
107+ result . push ( {
108+ start : currentPos ,
109+ end : startPos ,
110+ languageId : 'html'
111+ } ) ;
112+ }
113+ const end = Math . min ( region . end , endOffset ) ;
114+ const endPos = document . positionAt ( end ) ;
115+ if ( end > region . start ) {
116+ result . push ( {
117+ start : startPos ,
118+ end : endPos ,
119+ languageId : region . languageId ,
120+ attributeValue : region . attributeValue
121+ } ) ;
122+ }
123+ currentOffset = end ;
124+ currentPos = endPos ;
125+ }
126+ }
127+ if ( currentOffset < endOffset ) {
128+ const endPos = range ? range . end : document . positionAt ( endOffset ) ;
129+ result . push ( {
130+ start : currentPos ,
131+ end : endPos ,
132+ languageId : 'html'
133+ } ) ;
134+ }
135+ return result ;
136+ }
137+
138+ function getLanguagesInDocument ( _document : TextDocument , regions : EmbeddedRegion [ ] ) : string [ ] {
139+ const result = [ ] ;
140+ for ( const region of regions ) {
141+ if ( region . languageId && result . indexOf ( region . languageId ) === - 1 ) {
142+ result . push ( region . languageId ) ;
143+ if ( result . length === 3 ) {
144+ return result ;
145+ }
146+ }
147+ }
148+ result . push ( 'html' ) ;
149+ return result ;
150+ }
151+
152+ function getLanguageAtPosition ( document : TextDocument , regions : EmbeddedRegion [ ] , position : Position ) : string | undefined {
153+ const offset = document . offsetAt ( position ) ;
154+ for ( const region of regions ) {
155+ if ( region . start <= offset ) {
156+ if ( offset <= region . end ) {
157+ return region . languageId ;
158+ }
159+ } else {
160+ break ;
161+ }
162+ }
163+ return 'html' ;
164+ }
165+
166+ function getEmbeddedDocument ( document : TextDocument , contents : EmbeddedRegion [ ] , languageId : string , ignoreAttributeValues : boolean ) : TextDocument {
167+ let currentPos = 0 ;
168+ const oldContent = document . getText ( ) ;
169+ let result = '' ;
170+ let lastSuffix = '' ;
171+ for ( const c of contents ) {
172+ if ( c . languageId === languageId && ( ! ignoreAttributeValues || ! c . attributeValue ) ) {
173+ result = substituteWithWhitespace ( result , currentPos , c . start , oldContent , lastSuffix , getPrefix ( c ) ) ;
174+ result += oldContent . substring ( c . start , c . end ) ;
175+ currentPos = c . end ;
176+ lastSuffix = getSuffix ( c ) ;
177+ }
178+ }
179+ result = substituteWithWhitespace ( result , currentPos , oldContent . length , oldContent , lastSuffix , '' ) ;
180+ return TextDocument . create ( document . uri , languageId , document . version , result ) ;
181+ }
182+
183+ function getPrefix ( c : EmbeddedRegion ) {
184+ if ( c . attributeValue ) {
185+ switch ( c . languageId ) {
186+ case 'css' : return CSS_STYLE_RULE + '{' ;
187+ }
188+ }
189+ return '' ;
190+ }
191+ function getSuffix ( c : EmbeddedRegion ) {
192+ if ( c . attributeValue ) {
193+ switch ( c . languageId ) {
194+ case 'css' : return '}' ;
195+ case 'javascript' : return ';' ;
196+ }
197+ }
198+ return '' ;
199+ }
200+
201+ function substituteWithWhitespace ( result : string , start : number , end : number , oldContent : string , before : string , after : string ) {
202+ let accumulatedWS = 0 ;
203+ result += before ;
204+ for ( let i = start + before . length ; i < end ; i ++ ) {
205+ const ch = oldContent [ i ] ;
206+ if ( ch === '\n' || ch === '\r' ) {
207+ // only write new lines, skip the whitespace
208+ accumulatedWS = 0 ;
209+ result += ch ;
210+ } else {
211+ accumulatedWS ++ ;
212+ }
213+ }
214+ result = append ( result , ' ' , accumulatedWS - after . length ) ;
215+ result += after ;
216+ return result ;
217+ }
218+
219+ function append ( result : string , str : string , n : number ) : string {
220+ while ( n > 0 ) {
221+ if ( n & 1 ) {
222+ result += str ;
223+ }
224+ n >>= 1 ;
225+ str += str ;
226+ }
227+ return result ;
228+ }
229+
230+ function getAttributeLanguage ( attributeName : string ) : string | null {
231+ const match = attributeName . match ( / ^ ( s t y l e ) $ | ^ ( o n \w + ) $ / i) ;
232+ if ( ! match ) {
233+ return null ;
234+ }
235+ return match [ 1 ] ? 'css' : 'javascript' ;
236+ }
0 commit comments