written 0.0.5 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/LICENSE +8 -0
  4. data/README.md +63 -0
  5. data/Rakefile +17 -27
  6. data/lib/written.rb +0 -8
  7. data/lib/written/app/assets/images/written/placeholder.png +0 -0
  8. data/lib/written/app/assets/javascripts/written.coffee +2 -0
  9. data/lib/written/app/assets/javascripts/written/core/content.coffee +53 -35
  10. data/lib/written/app/assets/javascripts/written/core/cursor.coffee +33 -12
  11. data/lib/written/app/assets/javascripts/written/core/document.coffee +16 -11
  12. data/lib/written/app/assets/javascripts/written/core/extensions/string.coffee +9 -0
  13. data/lib/written/app/assets/javascripts/written/core/extensions/text.coffee +2 -0
  14. data/lib/written/app/assets/javascripts/written/core/history.coffee +2 -0
  15. data/lib/written/app/assets/javascripts/written/core/observer.coffee +6 -2
  16. data/lib/written/app/assets/javascripts/written/core/stringify.coffee +15 -0
  17. data/lib/written/app/assets/javascripts/written/parsers/block.coffee +69 -0
  18. data/lib/written/app/assets/javascripts/written/parsers/block/code.coffee +79 -15
  19. data/lib/written/app/assets/javascripts/written/parsers/block/heading.coffee +60 -5
  20. data/lib/written/app/assets/javascripts/written/parsers/block/image.coffee +103 -9
  21. data/lib/written/app/assets/javascripts/written/parsers/block/olist.coffee +94 -12
  22. data/lib/written/app/assets/javascripts/written/parsers/block/paragraph.coffee +63 -5
  23. data/lib/written/app/assets/javascripts/written/parsers/block/quote.coffee +92 -0
  24. data/lib/written/app/assets/javascripts/written/parsers/block/ulist.coffee +93 -12
  25. data/lib/written/app/assets/javascripts/written/parsers/inline.coffee +81 -0
  26. data/lib/written/app/assets/javascripts/written/parsers/inline/code.coffee +57 -0
  27. data/lib/written/app/assets/javascripts/written/parsers/inline/italic.coffee +40 -7
  28. data/lib/written/app/assets/javascripts/written/parsers/inline/link.coffee +43 -13
  29. data/lib/written/app/assets/javascripts/written/parsers/inline/list.coffee +27 -0
  30. data/lib/written/app/assets/javascripts/written/parsers/inline/strong.coffee +41 -7
  31. data/lib/written/app/assets/javascripts/written/parsers/parsers.coffee +21 -107
  32. data/lib/written/app/assets/javascripts/written/polyfills/CustomElements/CustomElements.js +32 -0
  33. data/lib/written/app/assets/javascripts/written/polyfills/CustomElements/base.js +40 -0
  34. data/lib/written/app/assets/javascripts/written/polyfills/CustomElements/boot.js +124 -0
  35. data/lib/written/app/assets/javascripts/written/polyfills/CustomElements/observe.js +318 -0
  36. data/lib/written/app/assets/javascripts/written/polyfills/CustomElements/register.js +369 -0
  37. data/lib/written/app/assets/javascripts/written/polyfills/CustomElements/traverse.js +86 -0
  38. data/lib/written/app/assets/javascripts/written/polyfills/CustomElements/upgrade.js +130 -0
  39. data/lib/written/app/assets/javascripts/written/polyfills/MutationObserver/MutationObserver.js +575 -0
  40. data/lib/written/app/assets/javascripts/written/polyfills/WeakMap/WeakMap.js +49 -0
  41. data/lib/written/app/assets/javascripts/written/polyfills/base.coffee +10 -0
  42. data/lib/written/app/assets/javascripts/written/polyfills/dom.js +104 -0
  43. data/lib/written/app/assets/javascripts/written/uploaders/aws.coffee +125 -0
  44. data/lib/written/app/assets/stylesheets/written.scss +80 -11
  45. data/lib/written/version.rb +1 -1
  46. data/test/server/app/assets/javascripts/application.coffee +20 -2
  47. data/test/server/app/assets/stylesheets/application.scss +2 -2
  48. data/test/server/app/assets/stylesheets/prism.css +0 -1
  49. data/test/server/app/views/posts/show.html.erb +10 -3
  50. metadata +26 -20
  51. data/lib/written/app/assets/javascripts/written/core/ext.coffee +0 -109
  52. data/lib/written/app/assets/javascripts/written/core/extensions.coffee +0 -2
  53. data/lib/written/document.rb +0 -42
  54. data/lib/written/node.rb +0 -21
  55. data/lib/written/nodes/code.rb +0 -65
  56. data/lib/written/nodes/heading.rb +0 -15
  57. data/lib/written/nodes/image.rb +0 -14
  58. data/lib/written/nodes/ordered_list.rb +0 -18
  59. data/lib/written/nodes/unordered_list.rb +0 -19
  60. data/lib/written/parsers.rb +0 -11
  61. data/lib/written/parsers/base.rb +0 -26
  62. data/lib/written/parsers/code.rb +0 -60
  63. data/lib/written/parsers/heading.rb +0 -19
  64. data/lib/written/parsers/image.rb +0 -19
  65. data/lib/written/parsers/link.rb +0 -12
  66. data/lib/written/parsers/list.rb +0 -33
  67. data/lib/written/parsers/word.rb +0 -16
@@ -23,7 +23,11 @@ class Written.Observer
23
23
 
24
24
  @pause(callback)
25
25
 
26
+ resume: =>
27
+ @mutations.observe @element(), @settings()
28
+
26
29
  pause: (callback) =>
27
30
  @mutations.disconnect()
28
- callback()
29
- @mutations.observe @element(), @settings()
31
+ if callback?
32
+ callback()
33
+ @resume()
@@ -0,0 +1,15 @@
1
+ ElementString = (node) ->
2
+ node.textContent
3
+
4
+ CustomElementString = (node) ->
5
+ node.toString()
6
+
7
+ TextNodeString = (node) ->
8
+ node.textContent
9
+
10
+ Written.Stringify = (node) ->
11
+ switch
12
+ when node.nodeType == Node.TEXT_NODE then TextNodeString(node)
13
+ when node.hasAttribute('is') then CustomElementString(node)
14
+ else ElementString(node)
15
+
@@ -0,0 +1,69 @@
1
+ class @Written.Parsers.Block
2
+ constructor: ->
3
+ @parsers = []
4
+
5
+ freeze: ->
6
+ if Written.Parsers.Block.parsers.default?
7
+ @parsers.push Written.Parsers.Block.parsers.default
8
+
9
+ Object.freeze(this)
10
+ Object.freeze(@parsers)
11
+
12
+ use: (names) ->
13
+ if typeof names == 'string' && names == 'all'
14
+ @parsers = Written.Parsers.Block.parsers.available.slice()
15
+ return this
16
+
17
+ if typeof names == 'string'
18
+ names = names.split(',').map (name) ->
19
+ name.trim()
20
+
21
+ parser = Written.Parsers.Block.parsers.available.find (obj) ->
22
+ names.contains(obj.parser.name)
23
+
24
+ if !parser?
25
+ throw "Couldn't find parser #{names}."
26
+ return this
27
+
28
+ @parsers.push(parser)
29
+ return this
30
+
31
+ parse: (lines) =>
32
+ blocks = []
33
+ while (line = lines.pop()) != undefined
34
+ str = line.toString()
35
+ block = blocks[blocks.length - 1]
36
+
37
+ if block? && block.multiline && block.accepts(line)
38
+ block.append(line)
39
+ continue
40
+
41
+ blocks.push(@find(str))
42
+
43
+ blocks
44
+
45
+ find: (str) ->
46
+ parser = undefined
47
+ for p in @parsers
48
+ if match = p.rule.exec(str)
49
+ parser = new p(match)
50
+ break
51
+
52
+ return parser
53
+
54
+
55
+ @Written.Parsers.Block.parsers = {
56
+ available: []
57
+ default: undefined
58
+ }
59
+
60
+ @Written.Parsers.Block.get = (name) ->
61
+ Written.Parsers.Block.parsers.available.find (p) ->
62
+ p.name.localeCompare(name) == 0
63
+
64
+ @Written.Parsers.Block.register = (parser, defaultParser = false) ->
65
+ if defaultParser
66
+ Written.Parsers.Block.parsers.default = parser
67
+ else
68
+ Written.Parsers.Block.parsers.available.push parser
69
+
@@ -1,26 +1,90 @@
1
1
  class Code
2
+ multiline: true
3
+
2
4
  constructor: (match) ->
3
- @match = match
4
- @node = "<pre data-status='opened' data-multiline='true'><code></code></pre>".toHTML()
5
+ @matches = [match]
6
+ @content = "\n"
7
+ @opened = true
8
+
9
+
10
+ accepts: (text) ->
11
+ @opened
12
+
13
+ append: (text) ->
14
+ match = /(~{3})$/i.exec(text)
15
+ if match?
16
+ @matches.push(match)
17
+ @opened = false
18
+ else
19
+ @content += text + "\n"
20
+
21
+ @content
22
+
23
+ processContent: (callback) =>
24
+
25
+ identical: (current, rendered) ->
26
+ current.outerHTML == rendered.outerHTML
27
+
28
+ markdown: =>
29
+ node = "<pre is='written-pre'><code></code></pre>".toHTML()
30
+ code = node.querySelector('code')
31
+
32
+ if @matches[0][3]?
33
+ code.classList.add("language-#{@matches[0][3]}")
34
+
35
+ code.appendChild(document.createTextNode(@content))
36
+ if @highlight?
37
+ @highlight(code)
38
+
39
+ code.insertAdjacentHTML('afterbegin', @matches[0][0])
40
+ if !@opened
41
+ code.insertAdjacentHTML('beforeend', @matches[1][0])
42
+
43
+ node
44
+
45
+ html: =>
46
+ node = "<pre><code></code></pre>".toHTML()
47
+ code = node.querySelector('code')
48
+
49
+ if @matches[0][3]?
50
+ code.classList.add("language-#{@matches[0][3]}")
51
+
52
+ if @matches[0][4]?
53
+ code.insertAdjacentHTML('beforebegin', "<header>#{@matches[0][4]}</header>")
54
+
55
+ code.appendChild(document.createTextNode(@content))
5
56
 
57
+ node
6
58
 
7
- valid: ->
8
- true
59
+ Code.rule = /^((~{3})([a-z]+)?)(?:\s(.*))?/i
9
60
 
10
- render: (text) =>
11
- if @match[3]?
12
- @node.querySelector('code').classList.add("language-#{@match[3]}")
61
+ Written.Parsers.Block.register Code
13
62
 
14
- code = @node.querySelector('code')
15
- code.appendChild(document.createTextNode(text + "\n"))
63
+ prototype = Object.create(HTMLPreElement.prototype)
16
64
 
17
- if /(~{3})$/i.test(text)
18
- @node.dataset.status = 'closed'
65
+ prototype.getRange = (offset, walker) ->
66
+ range = document.createRange()
19
67
 
20
- if Code.parseSyntax?
21
- Code.parseSyntax(code)
68
+ if !@firstChild?
69
+ range.setStart(this, 0)
70
+ else
71
+ while walker.nextNode()
72
+ if walker.currentNode.length < offset
73
+ offset -= walker.currentNode.length
74
+ continue
22
75
 
23
- @node
76
+ range.setStart(walker.currentNode, offset)
77
+ break
24
78
 
79
+ range.collapse(true)
80
+ range
81
+ prototype.toString = ->
82
+ if @textContent[@textContent.length - 1] == '\n'
83
+ @textContent.substr(0, @textContent.length - 1)
84
+ else
85
+ @textContent
25
86
 
26
- Written.Parsers.Block.register Code, /((~{3})([a-z]+)?)(.+)?/i
87
+ document.registerElement('written-pre', {
88
+ prototype: prototype
89
+ extends: 'pre'
90
+ })
@@ -1,10 +1,65 @@
1
1
  class Header
2
+ multiline: false
3
+
2
4
  constructor: (match) ->
3
5
  @match = match
4
6
 
5
- render: (text) =>
6
- size = @match[1].length
7
- return "<h#{size}>#{text}</h#{size}>".toHTML()
8
-
7
+ processContent: (callback) =>
8
+ if @content?
9
+ throw "Content Error: The content was already processed"
10
+ return
11
+
12
+ @content = callback(@match[3])
13
+
14
+ identical: (current, rendered) ->
15
+ current.outerHTML == rendered.outerHTML
16
+
17
+ markdown: =>
18
+ node = "<h#{@match[2].length} is='written-h#{@match[2].length}'>".toHTML()
19
+ for text in @content
20
+ if text.markdown?
21
+ node.appendChild(text.markdown())
22
+ else
23
+ node.appendChild(document.createTextNode(text))
24
+
25
+ node.insertAdjacentHTML('afterbegin', @match[1])
26
+ node
27
+
28
+ html: =>
29
+ node = "<h#{@match[2].length}>".toHTML()
30
+ for text in @content
31
+ if text.html?
32
+ node.appendChild(text.html())
33
+ else
34
+ node.appendChild(document.createTextNode(text))
35
+ node
36
+
37
+ Header.rule = /^((#{1,6})\s)(.*)$/i
38
+
39
+ Written.Parsers.Block.register Header
40
+
41
+ [1,2,3,4,5,6].forEach (size) ->
42
+ prototype = Object.create(HTMLHeadingElement.prototype)
43
+ prototype.getRange = (offset, walker) ->
44
+ range = document.createRange()
45
+
46
+ if !@firstChild?
47
+ range.setStart(this, 0)
48
+ else
49
+ while walker.nextNode()
50
+ if walker.currentNode.length < offset
51
+ offset -= walker.currentNode.length
52
+ continue
53
+
54
+ range.setStart(walker.currentNode, offset)
55
+ break
56
+
57
+ range.collapse(true)
58
+ range
59
+ prototype.toString = ->
60
+ @textContent
9
61
 
10
- Written.Parsers.Block.register Header, /^(#{1,6}) /i
62
+ document.registerElement("written-h#{size}", {
63
+ prototype: prototype
64
+ extends: "h#{size}"
65
+ })
@@ -1,18 +1,112 @@
1
1
  class Image
2
+ multiline: false
3
+
2
4
  constructor: (match) ->
3
5
  @match = match
4
- @figure = "<figure><div contenteditable='false'><img/></div><figcaption /></figure>".toHTML()
5
6
 
6
- render: =>
7
- caption = @figure.querySelector('figcaption')
8
- caption.appendChild document.createTextNode(@match[1])
9
- caption.appendChild document.createTextNode(@match[3])
7
+ processContent: (callback) =>
8
+ if @content?
9
+ throw "Content Error: The content was already processed"
10
+ return
11
+
12
+ @content = callback(@match[2])
13
+
14
+ identical: (current, rendered) ->
15
+
16
+ sameNode = (current, rendered) ->
17
+ current.nodeName == rendered.nodeName
18
+
19
+ sameImages = (current, rendered) ->
20
+ (current? && current.dataset.image) == (rendered? && rendered.dataset.image)
21
+
22
+ sameTexts = (current, rendered) ->
23
+ (current? && current.innerHTML) == (rendered? && rendered.innerHTML)
24
+
25
+
26
+ sameNode(current, rendered) &&
27
+ sameImages(current.querySelector('img'), rendered.querySelector('img')) &&
28
+ sameTexts(current.querySelector('figcaption'), rendered.querySelector('figcaption'))
29
+
30
+
31
+ markdown: =>
32
+ figure = "<figure is='written-figure'><div contenteditable='false'><div class='progress'></div><input type='file' /><img/></div><figcaption /></figure>".toHTML()
33
+ caption = figure.querySelector('figcaption')
34
+ container = figure.querySelector('div')
35
+
36
+ for text in @content
37
+ if text.markdown?
38
+ caption.appendChild(text.markdown())
39
+ else
40
+ caption.appendChild(document.createTextNode(text))
41
+
42
+ caption.insertAdjacentHTML('afterbegin', '![')
43
+ caption.insertAdjacentHTML('beforeend', ']')
44
+ caption.insertAdjacentHTML('beforeend', @match[3])
45
+
46
+ img = figure.querySelector('img')
10
47
  if @match[4]?
11
- caption.setAttribute('name', @match[4])
48
+ img.src = img.dataset.image = @match[4]
49
+ img.onerror = @placeholder.bind(this, img, true)
50
+ else
51
+ img.src = '/assets/written/placeholder.png'
52
+
53
+ if @configure?
54
+ @configure(figure)
55
+
56
+ figure
57
+
58
+ html: ->
59
+ figure = "<figure><div><img/></div><figcaption /></figure>".toHTML()
60
+ img = figure.querySelector('img')
61
+ caption = figure.querySelector('figcaption')
12
62
 
13
- img = @figure.querySelector('img')
14
63
  img.src = @match[4]
15
64
 
16
- @figure
65
+ for text in @content
66
+ if text.html?
67
+ caption.appendChild(text.html())
68
+ else
69
+ caption.appendChild(document.createTextNode(text))
70
+
71
+ figure
72
+
73
+ placeholder: (img, event, onerror = false) =>
74
+ img.src = '/assets/written/placeholder.png'
75
+ img.onerror = undefined
76
+ if onerror
77
+ img.classList.add 'error'
78
+
79
+ Image.rule = /^(!{1}\[([^\]]*)\])(\(([^\s]*)?\))$/i
80
+
81
+ Image.uploader = (uploader) ->
82
+ Image::configure = uploader.initialize
83
+
84
+ Written.Parsers.Block.register Image
85
+
86
+ prototype = Object.create(HTMLElement.prototype)
87
+
88
+ prototype.getRange = (offset, walker) ->
89
+ range = document.createRange()
90
+
91
+ if !@firstChild?
92
+ range.setStart(this, 0)
93
+ else
94
+ while walker.nextNode()
95
+ if walker.currentNode.length < offset
96
+ offset -= walker.currentNode.length
97
+ continue
98
+
99
+ range.setStart(walker.currentNode, offset)
100
+ break
101
+
102
+ range.collapse(true)
103
+ range
104
+
105
+ prototype.toString = ->
106
+ (@querySelector('figcaption') || this).textContent
107
+
108
+ document.registerElement('written-figure', {
109
+ prototype: prototype
110
+ extends: 'figure'
111
+ })
17
112
 
18
- Written.Parsers.Block.register Image, /^(!{1}\[([^\]]+)\])(\(([^\s]+)?\))$/i
@@ -1,18 +1,100 @@
1
1
  class OList
2
- rule: /^(\d\.\s)(.+)/i
2
+ multiline: true
3
+
3
4
  constructor: (match) ->
4
- @match = match
5
- @node = "<ol data-status='opened'></ol>".toHTML()
5
+ @matches = [match]
6
+ @opened = true
7
+
8
+ accepts: (text) ->
9
+ @opened = OList.rule.test(text)
10
+
11
+ @opened
12
+
13
+ append: (text) ->
14
+ @matches.push(OList.rule.exec(text))
15
+
16
+ processContent: (callback) =>
17
+ if @content?
18
+ throw "Content Error: The content was already processed"
19
+ return
20
+
21
+ lines = @matches.map (match) ->
22
+ match[2]
23
+
24
+ @content = callback(lines)
25
+
26
+ identical: (current, rendered) ->
27
+ current.outerHTML == rendered.outerHTML
28
+
29
+ markdown: =>
30
+ node = "<ol is='written-ol'></ol>".toHTML()
31
+ for line, index in @content
32
+ li = "<li is='written-li'>".toHTML()
33
+ li.appendChild(document.createTextNode(@matches[index][1]))
34
+
35
+ for text in line
36
+ if text.markdown?
37
+ li.appendChild(text.markdown())
38
+ else
39
+ li.appendChild(document.createTextNode(text.toString()))
40
+
41
+ node.appendChild(li)
42
+
43
+ node
44
+
45
+ html: =>
46
+ node = "<ol></ol>".toHTML()
47
+ for line, index in @content
48
+ li = "<li>".toHTML()
49
+
50
+ for text in line
51
+ if text.html?
52
+ li.appendChild(text.html())
53
+ else
54
+ li.appendChild(document.createTextNode(text.toString()))
55
+
56
+ node.appendChild(li)
57
+
58
+ node
59
+
60
+
61
+ OList.rule = /^(\d+\.\s)(.*)/i
62
+
63
+ Written.Parsers.Block.register OList
64
+
65
+ prototype = Object.create(HTMLOListElement.prototype)
66
+
67
+ prototype.toString = ->
68
+ texts = Array.prototype.slice.call(@children).map (li) ->
69
+ li.toString()
70
+ texts.join("\n")
71
+
72
+ prototype.getRange = (offset, walker) ->
73
+ range = document.createRange()
74
+ if !@firstChild?
75
+ range.setStart(this, 0)
76
+ return
77
+
78
+ li = this.firstElementChild
6
79
 
7
- valid: (text) ->
8
- valid = OList::rule.test(text)
9
- if !valid
10
- @node.dataset.status = false
11
- valid
80
+ while walker.nextNode()
81
+ if !li.contains(walker.currentNode)
82
+ newList = walker.currentNode
83
+ while newList? && !(newList instanceof HTMLLIElement)
84
+ newList = newList.parentElement
85
+ li = newList
86
+ offset--
12
87
 
13
- render: (text) =>
14
- @node.appendChild("<li>#{text}</li>".toHTML())
15
- @node
88
+ if walker.currentNode.length < offset
89
+ offset -= walker.currentNode.length
90
+ continue
91
+ range.setStart(walker.currentNode, offset)
92
+ break
16
93
 
17
- Written.Parsers.Block.register OList, OList::rule
94
+ range.collapse(true)
95
+ range
18
96
 
97
+ document.registerElement('written-ol', {
98
+ prototype: prototype
99
+ extends: 'ol'
100
+ })