1 | fs = require('fs')
|
2 | coffee = require('coffee-script')
|
3 |
|
4 | K = require('kcore')
|
5 |
|
6 | COMPACT = true
|
7 |
|
8 | LBH = '\n'
|
9 |
|
10 | LB = if COMPACT then '' else '\n'
|
11 |
|
12 |
|
13 | RESOURCE_CACHE_ON = true
|
14 | RESOURCE_CACHE = {}
|
15 | RESOURCE_COMPILED_CACHE = {}
|
16 |
|
17 | class 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 |
|
40 | class K.NateHtml
|
41 | constructor: (parent) ->
|
42 | @children = []
|
43 |
|
44 | delete: () ->
|
45 | ;
|
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 |
|
62 | class 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 |
|
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 |
|
104 | if @alternateLinksForLanguages?
|
105 | for langId, url of @alternateLinksForLanguages
|
106 | txt += '<link rel="alternate" hreflang="' + langId + '" href="' + url + '" />' + LB
|
107 |
|
108 |
|
109 | if @css != ''
|
110 | txt += '<style type="text/css">' + LB + @css + LB + '</style>' + LB
|
111 | txt += @htmlDocument.jsGetCollector('head').getAllAsTag()
|
112 |
|
113 |
|
114 | txt += '</head>' + LB
|
115 | return txt
|
116 |
|
117 | pushCss: (css) ->
|
118 | @css += css.trim() + LB
|
119 |
|
120 | class 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 |
|
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 |
|
182 |
|
183 |
|
184 |
|
185 | addResourceToLoad: (pipe, resourceData, done) ->
|
186 |
|
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 |
|
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 |
|
221 |
|
222 |
|
223 |
|
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 |
|
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 |
|
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 |
|
262 | class 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 | ;
|
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 |
|
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 |
|
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 |
|
455 | class 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
|