written 0.0.5 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ })