UNPKG

13.6 kBtext/coffeescriptView Raw
1fs = require('fs')
2coffee = require('coffee-script')
3
4K = require('kcore')
5
6COMPACT = true
7# hard LB - always present
8LBH = '\n'
9# soft LB - present only when COMPACT = false
10LB = if COMPACT then '' else '\n'
11
12# turn off the cache when you are rapidly developing js/css etc.
13RESOURCE_CACHE_ON = true
14RESOURCE_CACHE = {}
15RESOURCE_COMPILED_CACHE = {}
16
17class K.JsCollector
18 constructor: () ->
19 @js = ''
20
21 _generateJsTagFromText: (jsTxt) ->
22 return '<script language="javascript" type="text/javascript"><!--' + LBH + jsTxt + LBH + '--></script>' + LB
23
24 getAllAsTag: () ->
25 rv = ''
26 if @js != ''
27 rv = @_generateJsTagFromText(@js)
28 return rv
29
30 getAllAsOnLoadCode: () ->
31 rv = ''
32 if @js != ''
33 onLoadJs = '$(function() {' + LB + @js + LB + '});'
34 rv = @_generateJsTagFromText(onLoadJs)
35 return rv
36
37 pushFromTxt: (txt) ->
38 @js += txt.trim()
39
40class K.NateHtml
41 constructor: (parent) ->
42 @children = []
43
44 delete: () ->
45 ; # TODO: implement this, but at the moment we need it only in K.NateHtmlDom
46
47 newNate: (elem = null) ->
48 return new K.NateHtml(elem)
49
50 push: (child) ->
51 @children.push child
52
53 render: () ->
54 txt = ''
55 for child in @children
56 txt += child.render()
57 return txt
58
59 @setResourceCacheEnabled: (onOff) ->
60 RESOURCE_CACHE_ON = onOff
61
62class K.NateHtmlHead extends K.NateHtml
63 constructor: (parent) ->
64 super(parent)
65 @htmlDocument = parent
66 @css = ''
67 @meta = {}
68 @alternateLinksForLanguages = null
69
70 setTitle: (title) ->
71 @meta.title = title
72
73 setMetaAuthor: (author) ->
74 @meta.author = author
75
76 setMetaDescription: (description) ->
77 @meta.description = description
78
79 setMetaKeywords: (keywords) ->
80 @meta.keywords = keywords
81
82 setLinksForAlternateLangauges: (links) ->
83 @alternateLinksForLanguages = links
84
85 render: () ->
86 txt = '<head>' + LB
87
88 # title and metas
89 txt += '<meta charset="UTF-8">' + LB
90 if @meta.title?
91 txt += '<title>' + @meta.title + '</title>' + LB
92 if @meta.description?
93 txt += '<meta name="description" content="' + @meta.description + '">' + LB
94 if @meta.keywords?
95 txt += '<meta name="keywords" content="' + @meta.keywords + '">' + LB
96 if @meta.author?
97 txt += '<meta name="author" content="' + @meta.author + '">' + LB
98 txt += '<meta http-equiv="Cache-Control" content="no-cache"/>' + LB
99 txt += '<meta http-equiv="Expires" content="0"/>' + LB
100 txt += '<meta http-equiv="X-UA-Compatible" content="IE=Edge"/>' + LB
101 txt += '<meta name="viewport" content="width=device-width, initial-scale=1">' + LB
102
103 # alternate links for different langauges
104 if @alternateLinksForLanguages?
105 for langId, url of @alternateLinksForLanguages
106 txt += '<link rel="alternate" hreflang="' + langId + '" href="' + url + '" />' + LB
107
108 # css and js
109 if @css != ''
110 txt += '<style type="text/css">' + LB + @css + LB + '</style>' + LB
111 txt += @htmlDocument.jsGetCollector('head').getAllAsTag()
112
113 # finish
114 txt += '</head>' + LB
115 return txt
116
117 pushCss: (css) ->
118 @css += css.trim() + LB
119
120class K.NateHtmlDocument extends K.NateHtml
121 constructor: (parent) ->
122 super(parent)
123 @theHead = new K.NateHtmlHead(@)
124 @theBody = new K.NateHtmlBody(@)
125 @jsPipes =
126 head: new K.JsCollector()
127 bodyEnd: new K.JsCollector()
128 onLoad: new K.JsCollector()
129 @resourcesToLoad =
130 css: []
131 js_head: []
132 js_bodyEnd: []
133 js_onLoad: []
134 @resourcesToLoadCnt = 0
135 @resourcesLoadDoneCb = null
136 @waitingToEmit = false
137 @langId = null
138
139 # debugging helpers
140 if 0
141 @LOG_resourceLoading = console.log
142 else
143 @LOG_resourceLoading = () -> ;
144
145 head: () ->
146 return @theHead
147
148 body: () ->
149 return @theBody
150
151 render: () ->
152 if @langId?
153 langMeta = ' lang="' + @langId + '"'
154 else
155 langMeta = ''
156
157 txt = '<!DOCTYPE HTML>' + LB
158 txt += '<html' + langMeta + '>' + LB
159 txt += @theHead.render()
160 txt += @theBody.render()
161 txt += '</html>' + LB
162 return txt
163
164 setLang: (langId) ->
165 @langId = langId
166
167 loadResource: (resourceData, done) ->
168 if RESOURCE_CACHE_ON and RESOURCE_CACHE[resourceData.path]?
169 @LOG_resourceLoading '[Nate] RESOURCE_CACHE hit !'
170 done(RESOURCE_CACHE[resourceData.path])
171 else
172 @LOG_resourceLoading '[Nate] RESOURCE_CACHE miss !'
173 fs.readFile resourceData.path, 'utf8', (err, data) =>
174 if not err?
175 RESOURCE_CACHE[resourceData.path] = data
176 done(data)
177 else
178 console.log err
179 throw new Error('File loading failed from path:' + resourceData.path)
180
181 # important:
182 # done() callback is not called at the moment the resource is loaded as this may cause change of order
183 # (for example, loading: A-B-C, then calling done(): A-C-B, may cause a lot of problems)
184 # done() is called in right order after ALL resources are loaded
185 addResourceToLoad: (pipe, resourceData, done) ->
186 # add load request to loading-pipe
187 @resourcesToLoadCnt++
188 resourceData.loaded = false
189 resourceData.done = done
190 @resourcesToLoad[pipe].push resourceData
191 @LOG_resourceLoading '[Nate] add resource:', resourceData.path
192
193 # get from cache or trigger loading resource
194 @loadResource resourceData, (data) =>
195 resourceData.loaded = true
196 @resourcesToLoadCnt--
197 @LOG_resourceLoading '[Nate] loaded resource:', resourceData.path
198 resourceData.loadedContent = data
199 @_resourcesCheckForResourcesReady()
200
201 _resourcesCheckForResourcesReady: () ->
202 if (@waitingToEmit) and (@resourcesToLoadCnt == 0)
203 if @resourcesLoadDoneCb?
204 @_resourcesLoadDoneCb()
205 @_resourcesLoadDoneCb = null
206
207 _resourcesLoadDoneCb: () ->
208 @LOG_resourceLoading '[Nate] All resources loaded...'
209 for resourceType of @resourcesToLoad
210 @resourcesToLoadItem = @resourcesToLoad[resourceType]
211 for resourceItem in @resourcesToLoadItem
212 resourceItem.done(resourceItem.loadedContent)
213 @resourcesLoadDoneCb?()
214
215 cssFromStaticFile: (path, done) ->
216 @addResourceToLoad 'css', {path:path}, (data) =>
217 @theHead.pushCss(data)
218 done?()
219
220 # supported pipes are:
221 # head - goes to <head></head>
222 # bodyEnd - goes to bottom of <body></body>
223 # onLoad - goes to jQuery onLoad handler at the end of <body></body> section
224 jsGetCollector: (pipeName) ->
225 return @jsPipes[pipeName]
226
227 jsFromStaticFile: (path, pipeName, done) ->
228 K.Error.reportIfParameterNotSet(path, 'javascript path')
229 @addResourceToLoad 'js_' + pipeName, {path:path}, (data) =>
230 @jsGetCollector(pipeName).pushFromTxt(data)
231 done?()
232
233 coffeeFromStaticFile: (path, pipeName, done) ->
234 K.Error.reportIfParameterNotSet(path, 'coffeeScript path')
235 @addResourceToLoad 'js_' + pipeName, {path:path}, (data) =>
236 # compile or get from cache
237 if RESOURCE_CACHE_ON and RESOURCE_COMPILED_CACHE[path]?
238 compiled = RESOURCE_COMPILED_CACHE[path]
239 else
240 compiled = coffee.compile(data)
241 RESOURCE_COMPILED_CACHE[path] = compiled
242 # emit and done()
243 @jsGetCollector(pipeName).pushFromTxt(compiled)
244 done?()
245
246 _onReadyToEmit: (done) ->
247 @waitingToEmit = true
248 @resourcesLoadDoneCb = done
249 @_resourcesCheckForResourcesReady()
250
251 emitAsResponse: (httpResponse, cb) ->
252 @_onReadyToEmit () =>
253 responseBody = @render()
254 httpResponse.send(responseBody)
255 responseStats = {htmlSize: responseBody.length}
256 cb?(responseStats)
257
258 emitToString: (cb) ->
259 @_onReadyToEmit () =>
260 cb?(@render())
261
262class K.NateHtmlElem extends K.NateHtml
263 @NOTHING: 0
264 @JUST_CHILDREN: 1
265 @JUST_THE_INNER: 2
266 @OPEN_CLOSE_AT_ONCE: 3
267 @OPEN_THEN_CLOSE: 4
268 @ON_CLICK_EVENT_BLOCKER: 'var event=arguments[0] || window.event; var rv = (event.button != 0); event.returnValue = rv; return rv;'
269
270 constructor: (parent) ->
271 super(parent)
272 if parent?
273 parent.push(@)
274 else
275 @tagOpenCloseMode = K.NateHtmlElem.JUST_CHILDREN
276 @tagParamsList = {}
277 @enabled = true
278
279 deleteAllChildren: () ->
280 ; # TODO: implement this, but at the moment we need it only in K.NateHtmlDom
281
282 newNate: (parent) ->
283 return new K.NateHtmlElem(parent)
284
285 setTag: (@tagName, @tagParamsTxt, @tagInnerHTML, @tagOpenCloseMode) ->
286 if not @tagInnerHTML?
287 @tagInnerHTML = ''
288 if not @tagParamsTxt?
289 @tagParamsTxt = ''
290
291 tagOpenClose: (tag, txt, params) ->
292 rv = @newNate(@)
293 rv.setTag tag, params, txt, K.NateHtmlElem.OPEN_THEN_CLOSE
294 return rv
295
296 txt: (theTxt) ->
297 rv = @newNate(@)
298 rv.setTag null, null, theTxt, K.NateHtmlElem.JUST_THE_INNER
299 return rv
300
301 newTxt: (theTxt) ->
302 return @txt(theTxt)
303
304 br: () ->
305 return @txt '<br>'
306
307 newA: (txt, params) ->
308 return @tagOpenClose('a', txt, params)
309
310 newB: (txt, params) ->
311 return @tagOpenClose('b', txt, params)
312
313 newCanvas: (txt, params) ->
314 return @tagOpenClose('canvas', txt, params)
315
316 newDiv: (txt, params) ->
317 return @tagOpenClose('div', txt, params)
318
319 newH1: (txt, params) ->
320 return @tagOpenClose('h1', txt, params)
321
322 newH2: (txt, params) ->
323 return @tagOpenClose('h2', txt, params)
324
325 newH3: (txt, params) ->
326 return @tagOpenClose('h3', txt, params)
327
328 newImg: (src, params) ->
329 return @tagOpenClose('img', '', 'src="' + src + '" ' + params)
330
331 newInputCheckbox: (txt, params = '') ->
332 params = 'type="checkbox" ' + params
333 return @tagOpenClose('input', txt, params)
334
335 newInputPassword: (txt, params = '') ->
336 params = 'type="password" ' + params
337 return @tagOpenClose('input', txt, params)
338
339 newInputRadio: (txt, params = '') ->
340 params = 'type="radio" ' + params
341 return @tagOpenClose('input', txt, params)
342
343 newInputText: (txt, params = '') ->
344 params = 'type="text" ' + params
345 return @tagOpenClose('input', txt, params)
346
347 newInputSelect: (txt, params = '') ->
348 return @tagOpenClose('select', txt, params)
349
350 newLabel: (txt, params) ->
351 return @tagOpenClose('label', txt, params)
352
353 newLi: (txt, params) ->
354 return @tagOpenClose('li', txt, params)
355
356 newOption: (txt, params) ->
357 return @tagOpenClose('option', txt, params)
358
359 newP: (txt, params) ->
360 return @tagOpenClose('p', txt, params)
361
362 newScriptUrl: (src, params = '') ->
363 return @tagOpenClose('script', '', 'src="' + src + '" ' + params)
364
365 newSelect: (txt, params = '') ->
366 return @newInputSelect(txt, params)
367
368 newSpan: (txt, params) ->
369 return @tagOpenClose('span', txt, params)
370
371 newTextArea: (txt, params) ->
372 return @tagOpenClose('textarea', txt, params)
373
374 newUl: (txt, params) ->
375 return @tagOpenClose('ul', txt, params)
376
377 newTable: (txt, params) ->
378 return @tagOpenClose('table', txt, params)
379
380 newTBody: (txt, params) ->
381 return @tagOpenClose('tbody', txt, params)
382
383 newTr: (txt, params) ->
384 return @tagOpenClose('tr', txt, params)
385
386 newTd: (txt, params) ->
387 return @tagOpenClose('td', txt, params)
388
389 newTh: (txt, params) ->
390 return @tagOpenClose('th', txt, params)
391
392 setParams: (params) ->
393 @tagParamsTxt = params
394
395 setText: (txt) ->
396 @tagInnerHTML = txt
397 return @
398
399 setOnClickEventBlocker: () ->
400 @set('onclick', K.NateHtmlElem.ON_CLICK_EVENT_BLOCKER)
401 return @
402
403 set: (param, value) ->
404 switch param
405 when 'innerHTML'
406 @tagInnerHTML = value
407 when 'checked', 'selected'
408 if value
409 @tagParamsList[param] = param
410 else
411 if @tagParamsList[param]?
412 delete @tagParamsList[param]
413 when 'class', 'href', 'id', 'for', 'name', 'onclick', 'style', 'value', \
414 'rowSpan', 'colSpan'
415 # TODO: value should be string-escaped
416 @tagParamsList[param] = value
417 when 'on', 'enabled'
418 @enabled = value
419
420 return @
421
422 setData: (key, value) ->
423 @tagParamsList['data-' + key] = value
424 return @
425
426 #
427 # Rendering part
428 #
429 render: () ->
430 if @enabled
431 switch @tagOpenCloseMode
432 when K.NateHtmlElem.JUST_THE_INNER
433 rv = @tagInnerHTML
434 rv += super()
435 when K.NateHtmlElem.OPEN_CLOSE_AT_ONCE, K.NateHtmlElem.OPEN_THEN_CLOSE
436 rv = '<' + @tagName
437 if @tagParamsTxt isnt ''
438 rv += ' ' + @tagParamsTxt
439 for paramKey of @tagParamsList
440 rv += ' ' + paramKey + '="' + @tagParamsList[paramKey] + '"'
441 if @tagOpenClose == K.NateHtmlElem.OPEN_CLOSE_AT_ONCE
442 rv += '/>'
443 else
444 rv += '>'
445 rv += super()
446 rv += @tagInnerHTML
447 rv += '</' + @tagName + '>' + LB
448 when K.NateHtmlElem.JUST_CHILDREN
449 rv = super()
450 else
451 rv = ''
452
453 return rv
454
455class K.NateHtmlBody extends K.NateHtmlElem
456 constructor: (parent) ->
457 super(parent)
458 @htmlDocument = parent
459 @tagOpenCloseMode = K.NateHtmlElem.JUST_CHILDREN
460
461 render: () ->
462 txt = '<body'
463 if @tagParamsTxt? and @tagParamsTxt isnt ''
464 txt += ' ' + @tagParamsTxt
465 txt += '>'
466 txt += super()
467 txt += @htmlDocument.jsGetCollector('bodyEnd').getAllAsTag()
468 txt += @htmlDocument.jsGetCollector('onLoad').getAllAsOnLoadCode()
469 txt += '</body>'
470 return txt