written 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/lib/written/app/assets/javascripts/written/core/content.coffee +10 -10
  3. data/lib/written/app/assets/javascripts/written/core/cursor.coffee +6 -6
  4. data/lib/written/app/assets/javascripts/written/core/document.coffee +39 -6
  5. data/lib/written/app/assets/javascripts/written/parsers/block/code.coffee +34 -30
  6. data/lib/written/app/assets/javascripts/written/parsers/block/heading.coffee +28 -30
  7. data/lib/written/app/assets/javascripts/written/parsers/block/image.coffee +30 -49
  8. data/lib/written/app/assets/javascripts/written/parsers/block/olist.coffee +46 -49
  9. data/lib/written/app/assets/javascripts/written/parsers/block/paragraph.coffee +28 -32
  10. data/lib/written/app/assets/javascripts/written/parsers/block/quote.coffee +30 -32
  11. data/lib/written/app/assets/javascripts/written/parsers/block/ulist.coffee +43 -45
  12. data/lib/written/app/assets/javascripts/written/parsers/inline/code.coffee +28 -30
  13. data/lib/written/app/assets/javascripts/written/parsers/inline/italic.coffee +21 -25
  14. data/lib/written/app/assets/javascripts/written/parsers/inline/link.coffee +21 -25
  15. data/lib/written/app/assets/javascripts/written/parsers/inline/strong.coffee +21 -25
  16. data/lib/written/app/assets/javascripts/written/parsers/parsers.coffee +87 -19
  17. data/lib/written/app/assets/javascripts/written.coffee +0 -1
  18. data/lib/written/version.rb +1 -1
  19. data/test/server/app/assets/javascripts/application.coffee +5 -15
  20. metadata +2 -18
  21. data/lib/written/app/assets/javascripts/written/core/extensions/text.coffee +0 -2
  22. data/lib/written/app/assets/javascripts/written/core/stringify.coffee +0 -15
  23. data/lib/written/app/assets/javascripts/written/parsers/block.coffee +0 -69
  24. data/lib/written/app/assets/javascripts/written/parsers/inline/list.coffee +0 -27
  25. data/lib/written/app/assets/javascripts/written/parsers/inline.coffee +0 -81
  26. data/lib/written/app/assets/javascripts/written/polyfills/CustomElements/CustomElements.js +0 -32
  27. data/lib/written/app/assets/javascripts/written/polyfills/CustomElements/base.js +0 -40
  28. data/lib/written/app/assets/javascripts/written/polyfills/CustomElements/boot.js +0 -124
  29. data/lib/written/app/assets/javascripts/written/polyfills/CustomElements/observe.js +0 -318
  30. data/lib/written/app/assets/javascripts/written/polyfills/CustomElements/register.js +0 -369
  31. data/lib/written/app/assets/javascripts/written/polyfills/CustomElements/traverse.js +0 -86
  32. data/lib/written/app/assets/javascripts/written/polyfills/CustomElements/upgrade.js +0 -130
  33. data/lib/written/app/assets/javascripts/written/polyfills/MutationObserver/MutationObserver.js +0 -575
  34. data/lib/written/app/assets/javascripts/written/polyfills/WeakMap/WeakMap.js +0 -49
  35. data/lib/written/app/assets/javascripts/written/polyfills/base.coffee +0 -10
  36. data/lib/written/app/assets/javascripts/written/polyfills/dom.js +0 -104
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e36c2f64be41a65d8bac7fdde2e86d3b54529cb6
4
- data.tar.gz: 570b11448dc1be526eda6a768294b5d8c6423eae
3
+ metadata.gz: baeba731f6551380429d586b4cf9ce6958e87cc9
4
+ data.tar.gz: e8cb3235cc64c398e64cf7d87d3deee94f5eea47
5
5
  SHA512:
6
- metadata.gz: 0159af44f886957579f72d2006b04338a1b525cd51f3b68c51e6b8dbdc7987e9d766ee1f679d66262c8d5fc032656cd26cccdcf33def2a633548bfec7f7d1831
7
- data.tar.gz: a2107de3631865aaf514307c08ea4d6e1deeee36352f47d345a2aca0e070e66745c927cb040c192587815a1e4a4f8c97a11310db7cf9bb9120bf672749c176b6
6
+ metadata.gz: 904f533a3ab8b865659b82f7e26a10028f98708150facb7b7f9b8c0665aeabf38b8427329d6719c1bcc43ded3653c3b19db76594f99f37c99b02997bef00dc8d
7
+ data.tar.gz: 3e4db620d0eac31e9cb4252104fffbcede4660aadcd3f8bb5ad784bb17505e36c944f9579a356cbe2ba7f7efe4c63f00f22f596806e85a9f70ba2da3b252d3ff
@@ -26,21 +26,19 @@ class @Written
26
26
  initialize: (text, parsers) ->
27
27
  @observer.pause()
28
28
  if !parsers?
29
- parsers = Written.Parsers.default()
29
+ parsers = Written.Parsers
30
30
 
31
31
  @parsers = parsers
32
32
 
33
33
  if @element().contentEditable != 'true'
34
34
  @element().contentEditable = 'true'
35
35
 
36
- @parsers.freeze()
37
-
38
36
  document = new Written.Document(text, @parsers)
39
37
  cursor = new Written.Cursor(@element(), window.getSelection())
40
38
  document.cursor = cursor
41
39
 
42
40
 
43
- @update(document)
41
+ document.applyTo(@element())
44
42
  document.cursor.focus(document.toString().length)
45
43
 
46
44
  @history = new Written.History(document)
@@ -54,7 +52,9 @@ class @Written
54
52
  @element().dispatchEvent(event)
55
53
 
56
54
  changeTo: (text) =>
57
- @update(new Written.Document(text, @parsers))
55
+ document = new Written.Document(text, @parsers)
56
+ @history.push(document)
57
+ document.applyTo(@element())
58
58
 
59
59
  changed: (e) =>
60
60
  oldDocument = @history.current
@@ -63,7 +63,7 @@ class @Written
63
63
  if @element().children.length > 0 && oldDocument.toString().localeCompare(newDocument.toString()) == 0
64
64
  return
65
65
 
66
- @update(newDocument)
66
+ newDocument.applyTo(@element())
67
67
  @history.push(newDocument)
68
68
  @dispatch('written:changed', document: newDocument)
69
69
 
@@ -117,7 +117,7 @@ class @Written
117
117
  if cursor.offset < document.toString().length
118
118
  cursor.offset += 1
119
119
 
120
- @update(document)
120
+ document.applyTo(@element())
121
121
  @history.push(document)
122
122
 
123
123
 
@@ -130,7 +130,7 @@ class @Written
130
130
 
131
131
  if document = @history.previous()
132
132
  @history.current = document
133
- @update(@history.current)
133
+ document.applyTo(@element())
134
134
 
135
135
  redo: (e) =>
136
136
  if e.code == 'KeyZ' && e.metaKey && e.shiftKey
@@ -141,12 +141,12 @@ class @Written
141
141
 
142
142
  if document = @history.next()
143
143
  @history.current = document
144
- @update(@history.current)
144
+ @history.current.applyTo(@element())
145
145
 
146
146
  toString: =>
147
147
  texts = []
148
148
  for node in @element().childNodes
149
- content = node.toString().split('\n')
149
+ content = Written.Parsers.toString(node).split('\n')
150
150
  texts.push content.join('\n')
151
151
 
152
152
  texts.join '\n'
@@ -15,7 +15,7 @@ class Written.Cursor
15
15
  child = node.previousSibling
16
16
 
17
17
  while child
18
- @offset += Written.Stringify(child).length
18
+ @offset += Written.Parsers.toString(child).length
19
19
  child = child.previousSibling
20
20
 
21
21
  if node instanceof HTMLLIElement
@@ -27,7 +27,7 @@ class Written.Cursor
27
27
  for child in @element().children
28
28
  if child == node
29
29
  break
30
- @offset += Written.Stringify(child).length
30
+ @offset += Written.Parsers.toString(child).length
31
31
  @offset += 1
32
32
 
33
33
  @currentNode = ->
@@ -39,7 +39,7 @@ class Written.Cursor
39
39
 
40
40
  element = @element().firstElementChild
41
41
  while element && element != node
42
- offset -= Written.Stringify(element).length
42
+ offset -= Written.Parsers.toString(element).length
43
43
  element = element.nextElementSibling
44
44
 
45
45
  offset
@@ -53,12 +53,12 @@ class Written.Cursor
53
53
  if node is undefined
54
54
  node = @element().firstElementChild
55
55
 
56
- while node.nextElementSibling && Written.Stringify(node).length < offset
57
- offset -= Written.Stringify(node).length + 1
56
+ while node.nextElementSibling && Written.Parsers.toString(node).length < offset
57
+ offset -= Written.Parsers.toString(node).length + 1
58
58
  node = node.nextElementSibling
59
59
 
60
60
 
61
- range = node.getRange(Math.min(offset, Written.Stringify(node).length), document.createTreeWalker(node, NodeFilter.SHOW_TEXT))
61
+ range = Written.Parsers.getRange(node, Math.min(offset, Written.Parsers.toString(node).length), document.createTreeWalker(node, NodeFilter.SHOW_TEXT))
62
62
 
63
63
  if @offsetDiffersBetween(@selection, range)
64
64
  @selection.removeAllRanges()
@@ -1,13 +1,16 @@
1
1
  #= require ../parsers/parsers
2
2
 
3
3
  class Written.Document
4
- constructor: (textContent, parsers) ->
5
- @markdown = textContent
6
-
7
- @blocks = parsers.block.parse(textContent.split('\n').reverse())
4
+ constructor: (text, parsers) ->
5
+ @blocks = parsers.parse(parsers.blocks, text)
8
6
 
9
7
  @blocks.forEach (block) =>
10
- block.processContent(parsers.inline.parse.bind(parsers.inline))
8
+ if block.text?
9
+ if block.multiline
10
+ block.content = block.text().split('\n').map (text) ->
11
+ parsers.parse(parsers.inlines, text)
12
+ else
13
+ block.content = parsers.parse(parsers.inlines, block.text())
11
14
 
12
15
  freeze: =>
13
16
  Object.freeze(@blocks)
@@ -21,5 +24,35 @@ class Written.Document
21
24
 
22
25
  text
23
26
 
27
+ applyTo: (content) =>
28
+
29
+ for block, index in @blocks
30
+ remaining = Array.prototype.slice.call(content.children, index)
31
+ element = remaining[0]
32
+ node = @findNodeFor(block, remaining)
33
+
34
+ content.insertBefore(node, element)
35
+
36
+ elements = Array.prototype.slice.call(content.children, index)
37
+ for element in elements
38
+ element.remove()
39
+
40
+ @cursor.focus()
41
+
42
+ findNodeFor: (block, remaining) ->
43
+ node = block.markdown()
44
+
45
+ found = remaining.find (existing) ->
46
+ block.equals(existing, node)
47
+
48
+ found || node
49
+
24
50
  toString: =>
25
- @markdown
51
+ if @toString.cache?
52
+ return @toString.cache
53
+
54
+ texts = @blocks.map (block) ->
55
+ block.raw()
56
+
57
+ texts.join('\n')
58
+
@@ -1,5 +1,4 @@
1
1
  class Code
2
- @parserName: 'Code'
3
2
  multiline: true
4
3
 
5
4
  constructor: (match) ->
@@ -8,6 +7,11 @@ class Code
8
7
  @opened = true
9
8
 
10
9
 
10
+ raw: ->
11
+ texts = @matches.map (m) ->
12
+ m[0]
13
+ texts.join('\n')
14
+
11
15
  accepts: (text) ->
12
16
  @opened
13
17
 
@@ -17,17 +21,17 @@ class Code
17
21
  @matches.push(match)
18
22
  @opened = false
19
23
  else
24
+ @matches.push([text])
20
25
  @content += text + "\n"
21
26
 
22
27
  @content
23
28
 
24
- processContent: (callback) =>
25
29
 
26
- identical: (current, rendered) ->
30
+ equals: (current, rendered) ->
27
31
  current.outerHTML == rendered.outerHTML
28
32
 
29
33
  markdown: =>
30
- node = "<pre is='written-pre'><code></code></pre>".toHTML()
34
+ node = "<pre><code></code></pre>".toHTML()
31
35
  code = node.querySelector('code')
32
36
 
33
37
  if @matches[0][3]?
@@ -39,7 +43,7 @@ class Code
39
43
 
40
44
  code.insertAdjacentHTML('afterbegin', @matches[0][0])
41
45
  if !@opened
42
- code.insertAdjacentHTML('beforeend', @matches[1][0])
46
+ code.insertAdjacentHTML('beforeend', @matches[@matches.length - 1][0])
43
47
 
44
48
  node
45
49
 
@@ -59,33 +63,33 @@ class Code
59
63
 
60
64
  Code.rule = /^((~{3})([a-z]+)?)(?:\s(.*))?/i
61
65
 
62
- Written.Parsers.Block.register Code
63
-
64
- prototype = Object.create(HTMLPreElement.prototype)
66
+ Written.Parsers.register {
67
+ parser: Code
68
+ node: 'pre'
69
+ type: 'block'
70
+ getRange: (node, offset, walker) ->
71
+ range = document.createRange()
65
72
 
66
- prototype.getRange = (offset, walker) ->
67
- range = document.createRange()
73
+ if !node.firstChild?
74
+ range.setStart(node, 0)
75
+ else
76
+ while walker.nextNode()
77
+ if walker.currentNode.length < offset
78
+ offset -= walker.currentNode.length
79
+ continue
68
80
 
69
- if !@firstChild?
70
- range.setStart(this, 0)
71
- else
72
- while walker.nextNode()
73
- if walker.currentNode.length < offset
74
- offset -= walker.currentNode.length
75
- continue
81
+ range.setStart(walker.currentNode, offset)
82
+ break
76
83
 
77
- range.setStart(walker.currentNode, offset)
78
- break
84
+ range.collapse(true)
85
+ range
79
86
 
80
- range.collapse(true)
81
- range
82
- prototype.toString = ->
83
- if @textContent[@textContent.length - 1] == '\n'
84
- @textContent.substr(0, @textContent.length - 1)
85
- else
86
- @textContent
87
+ toString: (node) ->
88
+ if node.textContent[node.textContent.length - 1] == '\n'
89
+ node.textContent.substr(0, node.textContent.length - 1)
90
+ else
91
+ node.textContent
87
92
 
88
- document.registerElement('written-pre', {
89
- prototype: prototype
90
- extends: 'pre'
91
- })
93
+ highlightWith: (callback) ->
94
+ Code.prototype.highlight = callback
95
+ }
@@ -1,22 +1,21 @@
1
1
  class Header
2
- @parserName: 'Header'
3
2
  multiline: false
4
3
 
5
4
  constructor: (match) ->
6
5
  @match = match
7
6
 
8
- processContent: (callback) =>
9
- if @content?
10
- throw "Content Error: The content was already processed"
11
- return
7
+ equals: (current, rendered) ->
8
+ current.outerHTML == rendered.outerHTML
12
9
 
13
- @content = callback(@match[3])
10
+ text: ->
11
+ @match[3]
14
12
 
15
- identical: (current, rendered) ->
16
- current.outerHTML == rendered.outerHTML
13
+ raw: ->
14
+ @match[0]
17
15
 
18
16
  markdown: =>
19
- node = "<h#{@match[2].length} is='written-h#{@match[2].length}'>".toHTML()
17
+ node = "<h#{@match[2].length}>".toHTML()
18
+
20
19
  for text in @content
21
20
  if text.markdown?
22
21
  node.appendChild(text.markdown())
@@ -37,30 +36,29 @@ class Header
37
36
 
38
37
  Header.rule = /^((#{1,6})\s)(.*)$/i
39
38
 
40
- Written.Parsers.Block.register Header
41
39
 
42
40
  [1,2,3,4,5,6].forEach (size) ->
43
- prototype = Object.create(HTMLHeadingElement.prototype)
44
- prototype.getRange = (offset, walker) ->
45
- range = document.createRange()
41
+ Written.Parsers.register {
42
+ parser: Header
43
+ node: "h#{size}"
44
+ type: 'block'
45
+ getRange: (node, offset, walker) ->
46
+ range = document.createRange()
46
47
 
47
- if !@firstChild?
48
- range.setStart(this, 0)
49
- else
50
- while walker.nextNode()
51
- if walker.currentNode.length < offset
52
- offset -= walker.currentNode.length
53
- continue
48
+ if !node.firstChild?
49
+ range.setStart(node, 0)
50
+ else
51
+ while walker.nextNode()
52
+ if walker.currentNode.length < offset
53
+ offset -= walker.currentNode.length
54
+ continue
54
55
 
55
- range.setStart(walker.currentNode, offset)
56
- break
56
+ range.setStart(walker.currentNode, offset)
57
+ break
57
58
 
58
- range.collapse(true)
59
- range
60
- prototype.toString = ->
61
- @textContent
59
+ range.collapse(true)
60
+ range
62
61
 
63
- document.registerElement("written-h#{size}", {
64
- prototype: prototype
65
- extends: "h#{size}"
66
- })
62
+ toString: (node) ->
63
+ node.textContent
64
+ }
@@ -1,36 +1,24 @@
1
1
  class Image
2
- @parserName: 'Image'
3
2
  multiline: false
4
3
 
5
4
  constructor: (match) ->
6
5
  @match = match
7
6
 
8
- processContent: (callback) =>
9
- if @content?
10
- throw "Content Error: The content was already processed"
11
- return
7
+ raw: ->
8
+ @match[0]
12
9
 
13
- @content = callback(@match[2])
10
+ text: ->
11
+ @match[2]
14
12
 
15
- identical: (current, rendered) ->
16
-
17
- sameNode = (current, rendered) ->
18
- current.nodeName == rendered.nodeName
19
-
20
- sameImages = (current, rendered) ->
21
- (current? && current.dataset.image) == (rendered? && rendered.dataset.image)
22
-
23
- sameTexts = (current, rendered) ->
24
- (current? && current.innerHTML) == (rendered? && rendered.innerHTML)
25
-
26
-
27
- sameNode(current, rendered) &&
28
- sameImages(current.querySelector('img'), rendered.querySelector('img')) &&
29
- sameTexts(current.querySelector('figcaption'), rendered.querySelector('figcaption'))
13
+ equals: (current, rendered) ->
14
+ figcaption = current.querySelector('figcaption') || {}
15
+ img = current.querySelector('img') || {}
30
16
 
17
+ rendered.querySelector('figcaption').outerHTML == figcaption.outerHTML &&
18
+ rendered.querySelector('img').src == img.src
31
19
 
32
20
  markdown: =>
33
- figure = "<figure is='written-figure'><div contenteditable='false'><div class='progress'></div><input type='file' /><img/></div><figcaption /></figure>".toHTML()
21
+ figure = "<figure><div contenteditable='false'><input type='file' /><img/></div><figcaption /></figure>".toHTML()
34
22
  caption = figure.querySelector('figcaption')
35
23
  container = figure.querySelector('div')
36
24
 
@@ -47,7 +35,6 @@ class Image
47
35
  img = figure.querySelector('img')
48
36
  if @match[4]?
49
37
  img.src = img.dataset.image = @match[4]
50
- img.onerror = @placeholder.bind(this, img, true)
51
38
  else
52
39
  img.src = '/assets/written/placeholder.png'
53
40
 
@@ -74,40 +61,34 @@ class Image
74
61
  placeholder: (img, event, onerror = false) =>
75
62
  img.src = '/assets/written/placeholder.png'
76
63
  img.onerror = undefined
77
- if onerror
78
- img.classList.add 'error'
79
64
 
80
65
  Image.rule = /^(!{1}\[([^\]]*)\])(\(([^\s]*)?\))$/i
81
66
 
82
67
  Image.uploader = (uploader) ->
83
68
  Image::configure = uploader.initialize
84
69
 
85
- Written.Parsers.Block.register Image
86
70
 
87
- prototype = Object.create(HTMLElement.prototype)
71
+ Written.Parsers.register {
72
+ parser: Image
73
+ node: 'figure'
74
+ type: 'block'
75
+ getRange: (node, offset, walker) ->
76
+ range = document.createRange()
88
77
 
89
- prototype.getRange = (offset, walker) ->
90
- range = document.createRange()
91
-
92
- if !@firstChild?
93
- range.setStart(this, 0)
94
- else
95
- while walker.nextNode()
96
- if walker.currentNode.length < offset
97
- offset -= walker.currentNode.length
98
- continue
99
-
100
- range.setStart(walker.currentNode, offset)
101
- break
102
-
103
- range.collapse(true)
104
- range
78
+ if !node.firstChild?
79
+ range.setStart(this, 0)
80
+ else
81
+ while walker.nextNode()
82
+ if walker.currentNode.length < offset
83
+ offset -= walker.currentNode.length
84
+ continue
105
85
 
106
- prototype.toString = ->
107
- (@querySelector('figcaption') || this).textContent
86
+ range.setStart(walker.currentNode, offset)
87
+ break
108
88
 
109
- document.registerElement('written-figure', {
110
- prototype: prototype
111
- extends: 'figure'
112
- })
89
+ range.collapse(true)
90
+ range
113
91
 
92
+ toString: (node) ->
93
+ (node.querySelector('figcaption') || node).textContent
94
+ }
@@ -1,5 +1,4 @@
1
1
  class OList
2
- @parserName: 'OList'
3
2
  multiline: true
4
3
 
5
4
  constructor: (match) ->
@@ -14,23 +13,25 @@ class OList
14
13
  append: (text) ->
15
14
  @matches.push(OList.rule.exec(text))
16
15
 
17
- processContent: (callback) =>
18
- if @content?
19
- throw "Content Error: The content was already processed"
20
- return
16
+ equals: (current, rendered) ->
17
+ current.outerHTML == rendered.outerHTML
18
+
19
+ raw: ->
20
+ texts = @matches.map (match) ->
21
+ match[0]
22
+
23
+ texts.join('\n')
21
24
 
22
- lines = @matches.map (match) ->
25
+ text: ->
26
+ texts = @matches.map (match) ->
23
27
  match[2]
24
-
25
- @content = callback(lines)
26
28
 
27
- identical: (current, rendered) ->
28
- current.outerHTML == rendered.outerHTML
29
+ texts.join('\n')
29
30
 
30
31
  markdown: =>
31
- node = "<ol is='written-ol'></ol>".toHTML()
32
+ node = "<ol></ol>".toHTML()
32
33
  for line, index in @content
33
- li = "<li is='written-li'>".toHTML()
34
+ li = "<li>".toHTML()
34
35
  li.appendChild(document.createTextNode(@matches[index][1]))
35
36
 
36
37
  for text in line
@@ -61,41 +62,37 @@ class OList
61
62
 
62
63
  OList.rule = /^(\d+\.\s)(.*)/i
63
64
 
64
- Written.Parsers.Block.register OList
65
-
66
- prototype = Object.create(HTMLOListElement.prototype)
67
-
68
- prototype.toString = ->
69
- texts = Array.prototype.slice.call(@children).map (li) ->
70
- li.toString()
71
- texts.join("\n")
72
-
73
- prototype.getRange = (offset, walker) ->
74
- range = document.createRange()
75
- if !@firstChild?
76
- range.setStart(this, 0)
77
- return
78
-
79
- li = this.firstElementChild
80
-
81
- while walker.nextNode()
82
- if !li.contains(walker.currentNode)
83
- newList = walker.currentNode
84
- while newList? && !(newList instanceof HTMLLIElement)
85
- newList = newList.parentElement
86
- li = newList
87
- offset--
88
-
89
- if walker.currentNode.length < offset
90
- offset -= walker.currentNode.length
91
- continue
92
- range.setStart(walker.currentNode, offset)
93
- break
94
-
95
- range.collapse(true)
96
- range
65
+ Written.Parsers.register {
66
+ parser: OList
67
+ node: 'ol'
68
+ type: 'block'
69
+ getRange: (node, offset, walker) ->
70
+ range = document.createRange()
71
+ if !node.firstChild?
72
+ range.setStart(node, 0)
73
+ return
97
74
 
98
- document.registerElement('written-ol', {
99
- prototype: prototype
100
- extends: 'ol'
101
- })
75
+ li = node.firstElementChild
76
+
77
+ while walker.nextNode()
78
+ if !li.contains(walker.currentNode)
79
+ newList = walker.currentNode
80
+ while newList? && !(newList instanceof HTMLLIElement)
81
+ newList = newList.parentElement
82
+ li = newList
83
+ offset--
84
+
85
+ if walker.currentNode.length < offset
86
+ offset -= walker.currentNode.length
87
+ continue
88
+ range.setStart(walker.currentNode, offset)
89
+ break
90
+
91
+ range.collapse(true)
92
+ range
93
+
94
+ toString: (node) ->
95
+ texts = Array.prototype.slice.call(node.children).map (li) ->
96
+ li.textContent
97
+ texts.join("\n")
98
+ }