1+ import { HtmlParser } from 'angular2/src/compiler/html_parser' ;
2+ import { ParseSourceSpan , ParseError } from 'angular2/src/compiler/parse_util' ;
3+ import {
4+ HtmlAst ,
5+ HtmlAstVisitor ,
6+ HtmlElementAst ,
7+ HtmlAttrAst ,
8+ HtmlTextAst ,
9+ HtmlCommentAst ,
10+ htmlVisitAll
11+ } from 'angular2/src/compiler/html_ast' ;
12+ import { isPresent , isBlank } from 'angular2/src/facade/lang' ;
13+ import { StringMapWrapper } from 'angular2/src/facade/collection' ;
14+ import { Parser } from 'angular2/src/core/change_detection/parser/parser' ;
15+ import { Interpolation } from 'angular2/src/core/change_detection/parser/ast' ;
16+
17+ const I18N_ATTR = "i18n" ;
18+ const I18N_ATTR_PREFIX = "i18n-" ;
19+
20+ /**
21+ * A message extracted from a template.
22+ *
23+ * The identity of a message is comprised of `content` and `meaning`.
24+ *
25+ * `description` is additional information provided to the translator.
26+ */
27+ export class Message {
28+ constructor ( public content : string , public meaning : string , public description : string ) { }
29+ }
30+
31+ /**
32+ * All messages extracted from a template.
33+ */
34+ export class ExtractionResult {
35+ constructor ( public messages : Message [ ] , public errors : ParseError [ ] ) { }
36+ }
37+
38+ /**
39+ * An extraction error.
40+ */
41+ export class I18nExtractionError extends ParseError {
42+ constructor ( span : ParseSourceSpan , msg : string ) { super ( span , msg ) ; }
43+ }
44+
45+ /**
46+ * Removes duplicate messages.
47+ *
48+ * E.g.
49+ *
50+ * ```
51+ * var m = [new Message("message", "meaning", "desc1"), new Message("message", "meaning",
52+ * "desc2")];
53+ * expect(removeDuplicates(m)).toEqual([new Message("message", "meaning", "desc1")]);
54+ * ```
55+ */
56+ export function removeDuplicates ( messages : Message [ ] ) : Message [ ] {
57+ let uniq : { [ key : string ] : Message } = { } ;
58+ messages . forEach ( m => {
59+ let key = `$ng__${ m . meaning } __|${ m . content } ` ;
60+ if ( ! StringMapWrapper . contains ( uniq , key ) ) {
61+ uniq [ key ] = m ;
62+ }
63+ } ) ;
64+ return StringMapWrapper . values ( uniq ) ;
65+ }
66+
67+ /**
68+ * Extracts all messages from a template.
69+ *
70+ * It works like this. First, the extractor uses the provided html parser to get
71+ * the html AST of the template. Then it partitions the root nodes into parts.
72+ * Everything between two i18n comments becomes a single part. Every other nodes becomes
73+ * a part too.
74+ *
75+ * We process every part as follows. Say we have a part A.
76+ *
77+ * If the part has the i18n attribute, it gets converted into a message.
78+ * And we do not recurse into that part, except to extract messages from the attributes.
79+ *
80+ * If the part doesn't have the i18n attribute, we recurse into that part and
81+ * partition its children.
82+ *
83+ * While walking the AST we also remove i18n attributes from messages.
84+ */
85+ export class MessageExtractor {
86+ messages : Message [ ] ;
87+ errors : ParseError [ ] ;
88+
89+ constructor ( private _htmlParser : HtmlParser , private _parser : Parser ) { }
90+
91+ extract ( template : string , sourceUrl : string ) : ExtractionResult {
92+ this . messages = [ ] ;
93+ this . errors = [ ] ;
94+
95+ let res = this . _htmlParser . parse ( template , sourceUrl ) ;
96+ if ( res . errors . length > 0 ) {
97+ return new ExtractionResult ( [ ] , res . errors ) ;
98+ } else {
99+ let ps = this . _partition ( res . rootNodes ) ;
100+ ps . forEach ( p => this . _extractMessagesFromPart ( p ) ) ;
101+ return new ExtractionResult ( this . messages , this . errors ) ;
102+ }
103+ }
104+
105+ private _extractMessagesFromPart ( p : _Part ) : void {
106+ if ( p . hasI18n ) {
107+ this . messages . push ( new Message ( _stringifyNodes ( p . children , this . _parser ) , _meaning ( p . i18n ) ,
108+ _description ( p . i18n ) ) ) ;
109+ this . _recurseToExtractMessagesFromAttributes ( p . children ) ;
110+ } else {
111+ this . _recurse ( p . children ) ;
112+ }
113+
114+ if ( isPresent ( p . rootElement ) ) {
115+ this . _extractMessagesFromAttributes ( p . rootElement ) ;
116+ }
117+ }
118+
119+ private _recurse ( nodes : HtmlAst [ ] ) : void {
120+ let ps = this . _partition ( nodes ) ;
121+ ps . forEach ( p => this . _extractMessagesFromPart ( p ) ) ;
122+ }
123+
124+ private _recurseToExtractMessagesFromAttributes ( nodes : HtmlAst [ ] ) : void {
125+ nodes . forEach ( n => {
126+ if ( n instanceof HtmlElementAst ) {
127+ this . _extractMessagesFromAttributes ( n ) ;
128+ this . _recurseToExtractMessagesFromAttributes ( n . children ) ;
129+ }
130+ } ) ;
131+ }
132+
133+ private _extractMessagesFromAttributes ( p : HtmlElementAst ) : void {
134+ p . attrs . forEach ( attr => {
135+ if ( attr . name . startsWith ( I18N_ATTR_PREFIX ) ) {
136+ let expectedName = attr . name . substring ( 5 ) ;
137+ let matching = p . attrs . filter ( a => a . name == expectedName ) ;
138+
139+ if ( matching . length > 0 ) {
140+ let value = _removeInterpolation ( matching [ 0 ] . value , p . sourceSpan , this . _parser ) ;
141+ this . messages . push ( new Message ( value , _meaning ( attr . value ) , _description ( attr . value ) ) ) ;
142+ } else {
143+ this . errors . push (
144+ new I18nExtractionError ( p . sourceSpan , `Missing attribute '${ expectedName } '.` ) ) ;
145+ }
146+ }
147+ } ) ;
148+ }
149+
150+ // Man, this is so ugly!
151+ private _partition ( nodes : HtmlAst [ ] ) : _Part [ ] {
152+ let res = [ ] ;
153+
154+ for ( let i = 0 ; i < nodes . length ; ++ i ) {
155+ let n = nodes [ i ] ;
156+ let temp = [ ] ;
157+ if ( _isOpeningComment ( n ) ) {
158+ let i18n = ( < HtmlCommentAst > n ) . value . substring ( 5 ) . trim ( ) ;
159+ i ++ ;
160+ while ( ! _isClosingComment ( nodes [ i ] ) ) {
161+ temp . push ( nodes [ i ++ ] ) ;
162+ if ( i === nodes . length ) {
163+ this . errors . push (
164+ new I18nExtractionError ( n . sourceSpan , "Missing closing 'i18n' comment." ) ) ;
165+ break ;
166+ }
167+ }
168+ res . push ( new _Part ( null , temp , i18n , true ) ) ;
169+
170+ } else if ( n instanceof HtmlElementAst ) {
171+ let i18n = _findI18nAttr ( n ) ;
172+ res . push ( new _Part ( n , n . children , isPresent ( i18n ) ? i18n . value : null , isPresent ( i18n ) ) ) ;
173+ }
174+ }
175+
176+ return res ;
177+ }
178+ }
179+
180+ class _Part {
181+ constructor ( public rootElement : HtmlElementAst , public children : HtmlAst [ ] , public i18n : string ,
182+ public hasI18n : boolean ) { }
183+ }
184+
185+ function _isOpeningComment ( n : HtmlAst ) : boolean {
186+ return n instanceof HtmlCommentAst && isPresent ( n . value ) && n . value . startsWith ( "i18n:" ) ;
187+ }
188+
189+ function _isClosingComment ( n : HtmlAst ) : boolean {
190+ return n instanceof HtmlCommentAst && isPresent ( n . value ) && n . value == "/i18n" ;
191+ }
192+
193+ function _stringifyNodes ( nodes : HtmlAst [ ] , parser : Parser ) {
194+ let visitor = new _StringifyVisitor ( parser ) ;
195+ return htmlVisitAll ( visitor , nodes ) . join ( "" ) ;
196+ }
197+
198+ class _StringifyVisitor implements HtmlAstVisitor {
199+ constructor ( private _parser : Parser ) { }
200+
201+ visitElement ( ast : HtmlElementAst , context : any ) : any {
202+ let attrs = this . _join ( htmlVisitAll ( this , ast . attrs ) , " " ) ;
203+ let children = this . _join ( htmlVisitAll ( this , ast . children ) , "" ) ;
204+ return `<${ ast . name } ${ attrs } >${ children } </${ ast . name } >` ;
205+ }
206+
207+ visitAttr ( ast : HtmlAttrAst , context : any ) : any {
208+ if ( ast . name . startsWith ( I18N_ATTR_PREFIX ) ) {
209+ return "" ;
210+ } else {
211+ return `${ ast . name } ="${ ast . value } "` ;
212+ }
213+ }
214+
215+ visitText ( ast : HtmlTextAst , context : any ) : any {
216+ return _removeInterpolation ( ast . value , ast . sourceSpan , this . _parser ) ;
217+ }
218+
219+ visitComment ( ast : HtmlCommentAst , context : any ) : any { return "" ; }
220+
221+ private _join ( strs : string [ ] , str : string ) : string {
222+ return strs . filter ( s => s . length > 0 ) . join ( str ) ;
223+ }
224+ }
225+
226+ function _removeInterpolation ( value : string , source : ParseSourceSpan , parser : Parser ) : string {
227+ try {
228+ let parsed = parser . parseInterpolation ( value , source . toString ( ) ) ;
229+ if ( isPresent ( parsed ) ) {
230+ let ast : Interpolation = < any > parsed . ast ;
231+ let res = "" ;
232+ for ( let i = 0 ; i < ast . strings . length ; ++ i ) {
233+ res += ast . strings [ i ] ;
234+ if ( i != ast . strings . length - 1 ) {
235+ res += `{{I${ i } }}` ;
236+ }
237+ }
238+ return res ;
239+ } else {
240+ return value ;
241+ }
242+ } catch ( e ) {
243+ return value ;
244+ }
245+ }
246+
247+ function _findI18nAttr ( p : HtmlElementAst ) : HtmlAttrAst {
248+ let i18n = p . attrs . filter ( a => a . name == I18N_ATTR ) ;
249+ return i18n . length == 0 ? null : i18n [ 0 ] ;
250+ }
251+
252+ function _meaning ( i18n : string ) : string {
253+ if ( isBlank ( i18n ) || i18n == "" ) return null ;
254+ return i18n . split ( "|" ) [ 0 ] ;
255+ }
256+
257+ function _description ( i18n : string ) : string {
258+ if ( isBlank ( i18n ) || i18n == "" ) return null ;
259+ let parts = i18n . split ( "|" ) ;
260+ return parts . length > 1 ? parts [ 1 ] : null ;
261+ }
0 commit comments