written 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.babelrc +3 -0
  3. data/.gitignore +2 -0
  4. data/.ruby-version +1 -0
  5. data/README.md +18 -8
  6. data/Rakefile +1 -0
  7. data/compile-coffee.js +14 -0
  8. data/lib/written/app/assets/javascripts/written/attachments/image.coffee +4 -4
  9. data/lib/written/app/assets/javascripts/written/core/content.coffee +18 -29
  10. data/lib/written/app/assets/javascripts/written/core/cursor.coffee +8 -7
  11. data/lib/written/app/assets/javascripts/written/core/document.coffee +9 -8
  12. data/lib/written/app/assets/javascripts/written/core/{extensions/string.coffee → html.coffee} +3 -2
  13. data/lib/written/app/assets/javascripts/written/core/parsers.coffee +179 -0
  14. data/lib/written/app/assets/javascripts/written/parsers/block/code.coffee +11 -28
  15. data/lib/written/app/assets/javascripts/written/parsers/block/heading.coffee +37 -39
  16. data/lib/written/app/assets/javascripts/written/parsers/block/image.coffee +15 -39
  17. data/lib/written/app/assets/javascripts/written/parsers/block/olist.coffee +23 -20
  18. data/lib/written/app/assets/javascripts/written/parsers/block/paragraph.coffee +14 -34
  19. data/lib/written/app/assets/javascripts/written/parsers/block/quote.coffee +19 -38
  20. data/lib/written/app/assets/javascripts/written/parsers/block/ulist.coffee +20 -18
  21. data/lib/written/app/assets/javascripts/written/parsers/inline/bold.coffee +23 -0
  22. data/lib/written/app/assets/javascripts/written/parsers/inline/code.coffee +8 -27
  23. data/lib/written/app/assets/javascripts/written/parsers/inline/italic.coffee +8 -28
  24. data/lib/written/app/assets/javascripts/written/parsers/inline/link.coffee +8 -27
  25. data/lib/written/app/assets/stylesheets/written.scss +4 -0
  26. data/lib/written/version.rb +1 -1
  27. data/package.json +25 -0
  28. data/test/javascript/parsers/block/code.js +58 -0
  29. data/test/javascript/parsers/block/header.js +40 -0
  30. data/test/javascript/parsers/block/image.js +39 -0
  31. data/test/javascript/parsers/block/olist.js +41 -0
  32. data/test/javascript/parsers/block/paragraph.js +42 -0
  33. data/test/javascript/parsers/block/quote.js +41 -0
  34. data/test/javascript/parsers/block/ulist.js +40 -0
  35. data/test/javascript/parsers/inline/code.js +41 -0
  36. data/test/javascript/parsers/inline/italic.js +42 -0
  37. data/test/javascript/parsers/inline/link.js +42 -0
  38. data/test/javascript/parsers/inline/strong.js +41 -0
  39. data/test/server/app/assets/javascripts/application.coffee +8 -9
  40. data/test/server/app/assets/javascripts/secret.coffee +6 -0
  41. data/test/server/app/views/posts/show.html.erb +4 -4
  42. metadata +21 -12
  43. data/lib/written/app/assets/javascripts/written/parsers/inline/strong.coffee +0 -43
  44. data/lib/written/app/assets/javascripts/written/parsers/parsers.coffee +0 -95
  45. data/test/javascript/assertions/assert.coffee +0 -3
  46. data/test/javascript/polyfills.coffee +0 -2
  47. data/test/javascript/polyfills/HTMLULListElement.coffee +0 -0
  48. data/test/javascript/polyfills/Text.coffee +0 -0
  49. data/test/javascript/runner.coffee +0 -46
  50. data/test/javascript/tests/initialization.coffee +0 -16
  51. data/test/javascript/tests/parsing.coffee +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3feabb75dd783639d084dd9b58cb552d195a8a38
4
- data.tar.gz: cf8fbadf5551a7d453904460bc4ba4cb5b7f0676
3
+ metadata.gz: 0b65dbcbd89c707b5459e63d6e3126bd5e60dc62
4
+ data.tar.gz: 18f572157a4c5ba0335de9cfc0b7af2e80ff770e
5
5
  SHA512:
6
- metadata.gz: 43c5f5f0d01986530cdbbba47fcfecdafc6c07b367027b3413603aba5cb33fda21adfb34dff6f1263488eb9a6f4ca50d38ac1194a3eabfe5cef39556535c8370
7
- data.tar.gz: 10722bca8267b18cf3a268aefe0ec7bd82afb4445918b8f65c801ddf5484e0c8e212c1f7b0222351032b63fd643df4ac57c9b821e6488ac0b732d8916de65ea2
6
+ metadata.gz: 966a36308d380f0d119795a462e5d2c1c6c87f1cb6902707df6143ca6ba5f2dab1d60f4f95d9b19c4f301a19ae6cc3ba8afce49f2fca3f48e83e39fa7c89e945
7
+ data.tar.gz: b8ba365832818f9c86c43eeb628fc6822421257114e68eeb633b713fd7568a5dfb3d91edd48c99921a2390ceb7fee5fff38da17743aa664f75f7167df696a18e
data/.babelrc ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "presets": ["es2015"]
3
+ }
data/.gitignore CHANGED
@@ -2,3 +2,5 @@
2
2
  tmp/
3
3
  secrets.*
4
4
  *.log
5
+ node_modules
6
+ build/
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.3.0
data/README.md CHANGED
@@ -16,8 +16,7 @@ The editor also allows you to cherry-pick the markdown feature you wish to suppo
16
16
  To start using Written, you just have to create a new editor.
17
17
 
18
18
  ~~~javascript
19
- var editor = new Written(document.querySelector('#Editor'))
20
- editor.initialize()
19
+ new Written(document.querySelector('#Editor'))
21
20
  ~~~
22
21
 
23
22
  You can retrieve the document either as a markdown text, or a HTML string. For storage purposes, you might want to store the HTML string from the current document
@@ -34,13 +33,13 @@ Written allows you to enable the parsers you wish. The parsers are split into tw
34
33
  The editor needs to be configured before it is initialized. Here's how you customize the parsers.
35
34
 
36
35
  ~~~javascript
37
- var editor = new Written(document.querySelector('#Editor'))
38
- editor.parsers.use('block', ['Header', 'Code', 'UList', 'OList'])
39
- editor.parsers.use('inline', 'all')
40
- editor.initialize()
36
+ var parsers = new Written.Parsers({
37
+ blocks: ['header', 'code', 'ulist', 'olist']
38
+ })
39
+ var editor = new Written(document.querySelector('#Editor'), {parsers: parsers})
41
40
  ~~~
42
41
 
43
- You can specify ~~~ 'all'~~~ if you wish to use all the available parsers for a given type.
42
+ Not specifying parsers will enable *all parsers*.
44
43
 
45
44
  ### Document
46
45
 
@@ -52,7 +51,7 @@ Those documents are then stored in a history that Written then use to implement
52
51
 
53
52
  ### On Change Events
54
53
 
55
- Written dispatch an event whenever text changes on the Editor. To receive update when the editor change, just add an event listener to ~~~written:changed~~~ event.
54
+ Written dispatch an event whenever text changes on the Editor. To receive update when the editor change, just add an event listener to ```written:changed``` event.
56
55
 
57
56
  ~~~javascript
58
57
  document.addEventListener('written:changed', function(event) {
@@ -61,3 +60,14 @@ Written dispatch an event whenever text changes on the Editor. To receive update
61
60
  document.toString() // Markdown version
62
61
  })
63
62
  ~~~
63
+
64
+ ## Test the editor locally
65
+
66
+ If you want to test the editor on your machine, the easiest way is with [Ruby](httsp://www.ruby-lang.org).
67
+
68
+ ```ruby
69
+ $ bundle install
70
+ $ bundle exec rake server
71
+ ```
72
+
73
+ This will launch a server that runs the Editor with on ```localhost:3000```.
data/Rakefile CHANGED
@@ -62,6 +62,7 @@ task :compile do
62
62
  Bundler.require(:default, ENV['RAILS_ENV'])
63
63
 
64
64
  environment = Sprockets::Environment.new
65
+ environment.js_compressor = :uglifier
65
66
 
66
67
  environment.append_path 'lib/written/app/assets/javascripts'
67
68
  environment.append_path 'lib/written/app/assets/stylesheets'
data/compile-coffee.js ADDED
@@ -0,0 +1,14 @@
1
+ var coffee = require('coffee-script')
2
+ var babelJest = require("babel-jest");
3
+
4
+ module.exports = {
5
+ process: function(src, path) {
6
+ if (coffee.helpers.isCoffee(path)) {
7
+ return coffee.compile(src, {
8
+ 'bare': true
9
+ });
10
+ } else {
11
+ return babelJest.process(src, path);
12
+ }
13
+ }
14
+ };
@@ -6,7 +6,7 @@ class Image extends Written.Attachments.Base
6
6
  @node.querySelector('img').addEventListener 'click', @select.bind(this, @node)
7
7
 
8
8
  template: ->
9
- "<div id='WrittenOverlay' contenteditable=false>
9
+ Written.toHTML("<div id='WrittenOverlay' contenteditable=false>
10
10
  <div id='WrittenDialog'>
11
11
  <header>
12
12
  <div class='progress'></div>
@@ -19,7 +19,7 @@ class Image extends Written.Attachments.Base
19
19
  </figure>
20
20
  </div>
21
21
  </div>
22
- ".toHTML()
22
+ ")
23
23
 
24
24
  select: (node) =>
25
25
  @selection = @getSelection()
@@ -65,12 +65,12 @@ class Image extends Written.Attachments.Base
65
65
  dialog.classList.add 'failed'
66
66
  dialog.querySelector('h3').textContent = "Failed"
67
67
  dialog.querySelector('figure').remove()
68
- dialog.appendChild("
68
+ dialog.appendChild(Written.toHTML("
69
69
  <section>
70
70
  <p>An error occured while trying to process your image.</p>
71
71
  <button>Close</button>
72
72
  </section>
73
- ".toHTML())
73
+ "))
74
74
  dialog.querySelector('button').addEventListener('click', @cancel)
75
75
 
76
76
  upload: (e) =>
@@ -1,16 +1,10 @@
1
1
  class @Written
2
- constructor: (el) ->
2
+ constructor: (el, options = {}) ->
3
3
  el.instance = this
4
4
  el.dataset.editor = "written"
5
5
  @element = ->
6
6
  return el
7
7
 
8
- text = @toString()
9
- @element().textContent = ''
10
-
11
- @observer = new Written.Observer(@element(), @changed)
12
- @initialize = @initialize.bind(this, text)
13
-
14
8
  @element().addEventListener 'dragover', @over
15
9
  @element().addEventListener('drop', @preventDefaults)
16
10
 
@@ -19,35 +13,33 @@ class @Written
19
13
  @element().addEventListener('keydown', @redo)
20
14
  @element().addEventListener('keydown', @cursor)
21
15
 
22
-
23
- preventDefaults: (e) ->
24
- e.preventDefault()
25
-
26
- initialize: (text, parsers) ->
27
- @observer.pause()
16
+ parsers = options.parsers
28
17
  if !parsers?
29
- parsers = Written.Parsers
18
+ parsers = new Written.Parsers()
30
19
 
31
20
  @parsers = parsers
32
21
 
22
+ text = @toString()
23
+ @element().textContent = ''
24
+
33
25
  if @element().contentEditable != 'true'
34
26
  @element().contentEditable = 'true'
35
27
 
36
- document = new Written.Document(text, @parsers)
37
- cursor = new Written.Cursor(@element(), window.getSelection())
38
- document.cursor = cursor
28
+ cursor = new Written.Cursor(@element(), window.getSelection(), @parsers)
29
+ document = new Written.Document(text, @parsers, cursor)
39
30
 
40
-
41
31
  @render(document)
42
32
 
43
33
  document.cursor.focus(document.toString().length)
44
34
 
45
35
  @history = new Written.History(document)
46
-
36
+ @observer = new Written.Observer(@element(), @changed)
47
37
  @dispatch('written:initialized')
48
- @observer.resume()
49
38
 
50
39
 
40
+ preventDefaults: (e) ->
41
+ e.preventDefault()
42
+
51
43
  dispatch: (name, data = {}) =>
52
44
  event = new CustomEvent(name, bubbles: true, detail: data)
53
45
  @element().dispatchEvent(event)
@@ -59,8 +51,8 @@ class @Written
59
51
 
60
52
  changed: (e) =>
61
53
  oldDocument = @history.current
62
- newDocument = new Written.Document(@toString(), @parsers)
63
- newDocument.cursor = new Written.Cursor(@element(), window.getSelection())
54
+ cursor = new Written.Cursor(@element(), window.getSelection(), @parsers)
55
+ newDocument = new Written.Document(@toString(), @parsers, cursor)
64
56
  if @element().children.length > 0 && oldDocument.toString().localeCompare(newDocument.toString()) == 0
65
57
  return
66
58
 
@@ -70,14 +62,14 @@ class @Written
70
62
  @dispatch('written:changed', document: newDocument)
71
63
 
72
64
  cursor: =>
73
- @history.current.cursor = new Written.Cursor(@element(), window.getSelection())
65
+ @history.current.cursor = new Written.Cursor(@element(), window.getSelection(), @parsers)
74
66
 
75
67
  linefeed: (e) =>
76
68
  return unless e.which == 13
77
69
  e.preventDefault()
78
70
  e.stopPropagation()
79
71
 
80
- cursor = new Written.Cursor(@element(), window.getSelection())
72
+ cursor = new Written.Cursor(@element(), window.getSelection(), @parsers)
81
73
  @observer.pause =>
82
74
 
83
75
  offset = cursor.offset
@@ -94,8 +86,7 @@ class @Written
94
86
  lines.push('')
95
87
  cursor.offset += 1
96
88
 
97
- document = new Written.Document(lines.join('\n'), @parsers)
98
- document.cursor = cursor
89
+ document = new Written.Document(lines.join('\n'), @parsers, cursor)
99
90
  if cursor.offset < document.toString().length
100
91
  cursor.offset += 1
101
92
 
@@ -132,10 +123,8 @@ class @Written
132
123
  toString: =>
133
124
  texts = []
134
125
  for node in @element().childNodes
135
- content = Written.Parsers.toString(node).split('\n')
126
+ content = @parsers.toString(node).split('\n')
136
127
  texts.push content.join('\n')
137
128
 
138
129
  texts.join '\n'
139
130
 
140
-
141
- Written.Uploaders = {}
@@ -1,8 +1,9 @@
1
1
  class Written.Cursor
2
- constructor: (element, selection) ->
2
+ constructor: (element, selection, parsers) ->
3
3
  @element = ->
4
4
  element
5
5
  @selection = selection
6
+ @parsers = parsers
6
7
  children = Array.prototype.slice.call(@element().children, 0)
7
8
  @offset = selection.focusOffset
8
9
 
@@ -15,7 +16,7 @@ class Written.Cursor
15
16
  child = node.previousSibling
16
17
 
17
18
  while child
18
- @offset += Written.Parsers.toString(child).length
19
+ @offset += @parsers.toString(child).length
19
20
  child = child.previousSibling
20
21
 
21
22
  if node instanceof HTMLLIElement
@@ -27,7 +28,7 @@ class Written.Cursor
27
28
  for child in @element().children
28
29
  if child == node
29
30
  break
30
- @offset += Written.Parsers.toString(child).length
31
+ @offset += @parsers.toString(child).length
31
32
  @offset += 1
32
33
 
33
34
  @currentNode = ->
@@ -39,7 +40,7 @@ class Written.Cursor
39
40
 
40
41
  element = @element().firstElementChild
41
42
  while element && element != node
42
- offset -= Written.Parsers.toString(element).length
43
+ offset -= @parsers.toString(element).length
43
44
  element = element.nextElementSibling
44
45
 
45
46
  offset
@@ -53,12 +54,12 @@ class Written.Cursor
53
54
  if node is undefined
54
55
  node = @element().firstElementChild
55
56
 
56
- while node.nextElementSibling && Written.Parsers.toString(node).length < offset
57
- offset -= Written.Parsers.toString(node).length + 1
57
+ while node.nextElementSibling && @parsers.toString(node).length < offset
58
+ offset -= @parsers.toString(node).length + 1
58
59
  node = node.nextElementSibling
59
60
 
60
61
 
61
- range = Written.Parsers.getRange(node, Math.min(offset, Written.Parsers.toString(node).length), document.createTreeWalker(node, NodeFilter.SHOW_TEXT))
62
+ range = @parsers.getRange(node, Math.min(offset, @parsers.toString(node).length), document.createTreeWalker(node, NodeFilter.SHOW_TEXT))
62
63
 
63
64
  if @offsetDiffersBetween(@selection, range)
64
65
  @selection.removeAllRanges()
@@ -1,16 +1,17 @@
1
- #= require ../parsers/parsers
1
+ #= require ./parsers
2
2
 
3
3
  class Written.Document
4
- constructor: (text, parsers) ->
4
+ constructor: (text, parsers, cursor) ->
5
+ @cursor = cursor
5
6
  @blocks = parsers.parse(parsers.blocks, text)
6
7
 
7
8
  @blocks.forEach (block) =>
8
- if block.text?
9
+ if block.innerText?
9
10
  if block.multiline
10
- block.content = block.text().split('\n').map (text) ->
11
+ block.content = block.innerText().split('\n').map (text) ->
11
12
  parsers.parse(parsers.inlines, text)
12
13
  else
13
- block.content = parsers.parse(parsers.inlines, block.text())
14
+ block.content = parsers.parse(parsers.inlines, block.innerText())
14
15
 
15
16
  freeze: =>
16
17
  Object.freeze(@blocks)
@@ -20,7 +21,7 @@ class Written.Document
20
21
  text = ''
21
22
 
22
23
  @blocks.forEach (node) ->
23
- text += node.html().outerHTML + "\n"
24
+ text += node.toHTML().outerHTML + "\n"
24
25
 
25
26
  text
26
27
 
@@ -40,7 +41,7 @@ class Written.Document
40
41
  @cursor.focus()
41
42
 
42
43
  findNodeFor: (block, remaining) ->
43
- node = block.markdown()
44
+ node = block.toEditor()
44
45
 
45
46
  found = remaining.find (existing) ->
46
47
  block.equals(existing, node)
@@ -52,7 +53,7 @@ class Written.Document
52
53
  return @toString.cache
53
54
 
54
55
  texts = @blocks.map (block) ->
55
- block.raw()
56
+ block.outerText()
56
57
 
57
58
  texts.join('\n')
58
59
 
@@ -1,9 +1,10 @@
1
- String::toHTML = ->
1
+ Written.toHTML = (text) ->
2
2
  el = document.createElement('div')
3
- el.innerHTML = this
3
+ el.innerHTML = text
4
4
  if el.children.length > 1
5
5
  el.children
6
6
  else
7
7
  el.children[0]
8
8
 
9
9
 
10
+
@@ -0,0 +1,179 @@
1
+ class @Written.Parsers
2
+ constructor: (parsers = {}) ->
3
+ if parsers.blocks?
4
+ @blocks = Written.Parsers.Blocks.select(parsers.blocks)
5
+ else
6
+ @blocks = [Written.Parsers.Blocks.default].concat(Written.Parsers.Blocks)
7
+
8
+ if parsers.inlines?
9
+ @inlines = Written.Parsers.Inlines.select(parsers.inlines)
10
+ else
11
+ @inlines = Written.Parsers.Inlines
12
+
13
+ @nodes = {}
14
+
15
+ for struct in @blocks.concat(@inlines)
16
+ for node in struct.nodes
17
+ @nodes[node] = struct
18
+
19
+ @blocks.parse = @parseBlocks.bind(this, @blocks)
20
+ @inlines.parse = @parseInlines.bind(this, @inlines)
21
+
22
+ parse: (parsers, text) ->
23
+ parsers.parse(text)
24
+
25
+ parseBlocks: (parsers, text) ->
26
+ parsers = [parsers.default].concat(parsers).reverse()
27
+ blocks = []
28
+ lines = text.split('\n').reverse()
29
+ while (line = lines.pop()) != undefined
30
+ str = line
31
+ block = blocks[blocks.length - 1]
32
+
33
+ if block? && block.multiline && block.accepts(line)
34
+ block.append(line)
35
+ continue
36
+
37
+ blocks.push(@find(parsers, str))
38
+
39
+ blocks
40
+
41
+ parseInlines: (parsers, text) ->
42
+ buffer = ''
43
+ content = []
44
+ matches = []
45
+ index = 0
46
+
47
+ for struct in parsers
48
+ struct.rule.lastIndex = 0
49
+
50
+ while match = struct.rule.exec(text)
51
+ parser = new struct.parser(match)
52
+ matches[parser.index()] = parser
53
+
54
+ while text[index]?
55
+ if parser = matches[index]
56
+ content.push buffer.slice(0)
57
+ content.push parser
58
+ buffer = ''
59
+ index += parser.length()
60
+ else
61
+ buffer += text[index]
62
+ index += 1
63
+
64
+ if buffer.length > 0
65
+ content.push buffer
66
+
67
+ content
68
+
69
+
70
+ find: (parsers, str) ->
71
+ parser = undefined
72
+ for struct in parsers
73
+ if match = struct.rule.exec(str)
74
+ parser = new struct.parser(match)
75
+ break
76
+
77
+ return parser
78
+
79
+ get: (name) ->
80
+ @nodes[name]
81
+
82
+ getRange: (node, offset, walker) ->
83
+ @nodes[node.nodeName.toLowerCase()].getRange(node, offset, walker)
84
+
85
+ toString: (node) ->
86
+ struct = @nodes[node.nodeName.toLowerCase()]
87
+ if struct?
88
+ struct.toString(node)
89
+ else
90
+ node.textContent
91
+
92
+ Written.Parsers.Blocks = []
93
+ Written.Parsers.Inlines = []
94
+
95
+ Written.Parsers.Blocks.select = (nodes) ->
96
+ selected = []
97
+ nodes.map (name) ->
98
+ struct = Written.Parsers.Blocks.find (struct) ->
99
+ struct.name == name
100
+ if struct?
101
+ selected.push struct
102
+
103
+ [Written.Parsers.Blocks.default].concat(selected)
104
+
105
+ Written.Parsers.Inlines.select = (nodes) ->
106
+ selected = []
107
+ nodes.map (name) ->
108
+ struct = Written.Parsers.Inlines.find (struct) ->
109
+ struct.name == name
110
+ if struct?
111
+ selected.push struct
112
+
113
+ selected
114
+
115
+ Written.Parsers.register = (struct) ->
116
+ type = undefined
117
+ if struct.type == 'block'
118
+ type = Written.Parsers.Blocks
119
+ else if struct.type == 'inline'
120
+ type = Written.Parsers.Inlines
121
+ else
122
+ raise 'error: Written.Parsers can either be "block" or "inline"'
123
+ return
124
+
125
+ Written.Parsers.normalize(struct)
126
+ if struct.default
127
+ type.default = struct
128
+ else
129
+ type.push struct
130
+
131
+
132
+ Written.Parsers.normalize = (struct) ->
133
+ if !struct.getRange
134
+ struct.getRange = (node, offset, walker) ->
135
+ range = document.createRange()
136
+
137
+ if !node.firstChild?
138
+ range.setStart(node, 0)
139
+ else
140
+ while walker.nextNode()
141
+ if walker.currentNode.length < offset
142
+ offset -= walker.currentNode.length
143
+ continue
144
+
145
+ range.setStart(walker.currentNode, offset)
146
+ break
147
+
148
+ range.collapse(true)
149
+ range
150
+
151
+ if Object.prototype.toString == struct.toString
152
+ struct.toString = (node) ->
153
+ node.textContent
154
+
155
+ class Written.Parsers.Block
156
+ outerText: ->
157
+ throw "method implementation: #{this.name}.outerText() is missing."
158
+
159
+ equals: ->
160
+ throw "method implementation: #{this.name}.equals(current, rendered) is missing."
161
+
162
+ toEditor: ->
163
+ throw "method implementation: #{this.name}.toEditor() is missing."
164
+
165
+ toHTML: ->
166
+ throw "method implementation: #{this.name}.toHTML() is missing."
167
+
168
+ class Written.Parsers.Inline
169
+ index: ->
170
+ throw "method implementation: #{this.name}.index() is missing."
171
+
172
+ length: ->
173
+ throw "method implementation: #{this.name}.length() is missing."
174
+
175
+ toEditor: ->
176
+ throw "method implementation: #{this.name}.toEditor() is missing."
177
+
178
+ toHTML: ->
179
+ throw "method implementation: #{this.name}.toHTML() is missing."