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 | httpResponse.send @render()
|
254 | cb?()
|
255 |
|
256 | emitToString: (cb) ->
|
257 | @_onReadyToEmit () =>
|
258 | cb?(@render())
|
259 |
|
260 | class K.NateHtmlElem extends K.NateHtml
|
261 | @NOTHING: 0
|
262 | @JUST_CHILDREN: 1
|
263 | @JUST_THE_INNER: 2
|
264 | @OPEN_CLOSE_AT_ONCE: 3
|
265 | @OPEN_THEN_CLOSE: 4
|
266 | @ON_CLICK_EVENT_BLOCKER: 'var event=arguments[0] || window.event; var rv = (event.button != 0); event.returnValue = rv; return rv;'
|
267 |
|
268 | constructor: (parent) ->
|
269 | super(parent)
|
270 | if parent?
|
271 | parent.push(@)
|
272 | else
|
273 | @tagOpenCloseMode = K.NateHtmlElem.JUST_CHILDREN
|
274 | @tagParamsList = {}
|
275 | @enabled = true
|
276 |
|
277 | deleteAllChildren: () ->
|
278 | ;
|
279 |
|
280 | newNate: (parent) ->
|
281 | return new K.NateHtmlElem(parent)
|
282 |
|
283 | setTag: (@tagName, @tagParamsTxt, @tagInnerHTML, @tagOpenCloseMode) ->
|
284 | if not @tagInnerHTML?
|
285 | @tagInnerHTML = ''
|
286 | if not @tagParamsTxt?
|
287 | @tagParamsTxt = ''
|
288 |
|
289 | tagOpenClose: (tag, txt, params) ->
|
290 | rv = @newNate(@)
|
291 | rv.setTag tag, params, txt, K.NateHtmlElem.OPEN_THEN_CLOSE
|
292 | return rv
|
293 |
|
294 | txt: (theTxt) ->
|
295 | rv = @newNate(@)
|
296 | rv.setTag null, null, theTxt, K.NateHtmlElem.JUST_THE_INNER
|
297 | return rv
|
298 |
|
299 | newTxt: (theTxt) ->
|
300 | return @txt(theTxt)
|
301 |
|
302 | br: () ->
|
303 | return @txt '</br>'
|
304 |
|
305 | newA: (txt, params) ->
|
306 | return @tagOpenClose('a', txt, params)
|
307 |
|
308 | newB: (txt, params) ->
|
309 | return @tagOpenClose('b', txt, params)
|
310 |
|
311 | newCanvas: (txt, params) ->
|
312 | return @tagOpenClose('canvas', txt, params)
|
313 |
|
314 | newDiv: (txt, params) ->
|
315 | return @tagOpenClose('div', txt, params)
|
316 |
|
317 | newH1: (txt, params) ->
|
318 | return @tagOpenClose('h1', txt, params)
|
319 |
|
320 | newH2: (txt, params) ->
|
321 | return @tagOpenClose('h2', txt, params)
|
322 |
|
323 | newH3: (txt, params) ->
|
324 | return @tagOpenClose('h3', txt, params)
|
325 |
|
326 | newImg: (src, params) ->
|
327 | return @tagOpenClose('img', '', 'src="' + src + '" ' + params)
|
328 |
|
329 | newInputCheckbox: (txt, params = '') ->
|
330 | params = 'type="checkbox" ' + params
|
331 | return @tagOpenClose('input', txt, params)
|
332 |
|
333 | newInputPassword: (txt, params = '') ->
|
334 | params = 'type="password" ' + params
|
335 | return @tagOpenClose('input', txt, params)
|
336 |
|
337 | newInputRadio: (txt, params = '') ->
|
338 | params = 'type="radio" ' + params
|
339 | return @tagOpenClose('input', txt, params)
|
340 |
|
341 | newInputText: (txt, params = '') ->
|
342 | params = 'type="text" ' + params
|
343 | return @tagOpenClose('input', txt, params)
|
344 |
|
345 | newInputSelect: (txt, params = '') ->
|
346 | return @tagOpenClose('select', txt, params)
|
347 |
|
348 | newLabel: (txt, params) ->
|
349 | return @tagOpenClose('label', txt, params)
|
350 |
|
351 | newLi: (txt, params) ->
|
352 | return @tagOpenClose('li', txt, params)
|
353 |
|
354 | newOption: (txt, params) ->
|
355 | return @tagOpenClose('option', txt, params)
|
356 |
|
357 | newP: (txt, params) ->
|
358 | return @tagOpenClose('p', txt, params)
|
359 |
|
360 | newSelect: (txt, params = '') ->
|
361 | return @newInputSelect(txt, params)
|
362 |
|
363 | newSpan: (txt, params) ->
|
364 | return @tagOpenClose('span', txt, params)
|
365 |
|
366 | newTextArea: (txt, params) ->
|
367 | return @tagOpenClose('textarea', txt, params)
|
368 |
|
369 | newUl: (txt, params) ->
|
370 | return @tagOpenClose('ul', txt, params)
|
371 |
|
372 | newTable: (txt, params) ->
|
373 | return @tagOpenClose('table', txt, params)
|
374 |
|
375 | newTBody: (txt, params) ->
|
376 | return @tagOpenClose('tbody', txt, params)
|
377 |
|
378 | newTr: (txt, params) ->
|
379 | return @tagOpenClose('tr', txt, params)
|
380 |
|
381 | newTd: (txt, params) ->
|
382 | return @tagOpenClose('td', txt, params)
|
383 |
|
384 | setParams: (params) ->
|
385 | @tagParamsTxt = params
|
386 |
|
387 | setText: (txt) ->
|
388 | @tagInnerHTML = txt
|
389 | return @
|
390 |
|
391 | setOnClickEventBlocker: () ->
|
392 | @set('onclick', K.NateHtmlElem.ON_CLICK_EVENT_BLOCKER)
|
393 | return @
|
394 |
|
395 | set: (param, value) ->
|
396 | switch param
|
397 | when 'innerHTML'
|
398 | @tagInnerHTML = value
|
399 | when 'checked', 'selected'
|
400 | if value
|
401 | @tagParamsList[param] = param
|
402 | else
|
403 | if @tagParamsList[param]?
|
404 | delete @tagParamsList[param]
|
405 | when 'class', 'href', 'id', 'for', 'name', 'onclick', 'style', 'value', \
|
406 | 'rowSpan', 'colSpan'
|
407 |
|
408 | @tagParamsList[param] = value
|
409 | when 'on', 'enabled'
|
410 | @enabled = value
|
411 |
|
412 | return @
|
413 |
|
414 | setData: (key, value) ->
|
415 | @tagParamsList['data-' + key] = value
|
416 | return @
|
417 |
|
418 |
|
419 |
|
420 |
|
421 | render: () ->
|
422 | if @enabled
|
423 | switch @tagOpenCloseMode
|
424 | when K.NateHtmlElem.JUST_THE_INNER
|
425 | rv = @tagInnerHTML
|
426 | rv += super()
|
427 | when K.NateHtmlElem.OPEN_CLOSE_AT_ONCE, K.NateHtmlElem.OPEN_THEN_CLOSE
|
428 | rv = '<' + @tagName
|
429 | if @tagParamsTxt isnt ''
|
430 | rv += ' ' + @tagParamsTxt
|
431 | for paramKey of @tagParamsList
|
432 | rv += ' ' + paramKey + '="' + @tagParamsList[paramKey] + '"'
|
433 | if @tagOpenClose == K.NateHtmlElem.OPEN_CLOSE_AT_ONCE
|
434 | rv += '/>'
|
435 | else
|
436 | rv += '>'
|
437 | rv += super()
|
438 | rv += @tagInnerHTML
|
439 | rv += '</' + @tagName + '>' + LB
|
440 | when K.NateHtmlElem.JUST_CHILDREN
|
441 | rv = super()
|
442 | else
|
443 | rv = ''
|
444 |
|
445 | return rv
|
446 |
|
447 | class K.NateHtmlBody extends K.NateHtmlElem
|
448 | constructor: (parent) ->
|
449 | super(parent)
|
450 | @htmlDocument = parent
|
451 | @tagOpenCloseMode = K.NateHtmlElem.JUST_CHILDREN
|
452 |
|
453 | render: () ->
|
454 | txt = '<body'
|
455 | if @tagParamsTxt? and @tagParamsTxt isnt ''
|
456 | txt += ' ' + @tagParamsTxt
|
457 | txt += '>'
|
458 | txt += super()
|
459 | txt += @htmlDocument.jsGetCollector('bodyEnd').getAllAsTag()
|
460 | txt += @htmlDocument.jsGetCollector('onLoad').getAllAsOnLoadCode()
|
461 | txt += '</body>'
|
462 | return txt
|