written 0.2.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.babelrc +3 -0
- data/.gitignore +2 -0
- data/.ruby-version +1 -0
- data/README.md +18 -8
- data/Rakefile +1 -0
- data/compile-coffee.js +14 -0
- data/lib/written/app/assets/javascripts/written/attachments/image.coffee +4 -4
- data/lib/written/app/assets/javascripts/written/core/content.coffee +18 -29
- data/lib/written/app/assets/javascripts/written/core/cursor.coffee +8 -7
- data/lib/written/app/assets/javascripts/written/core/document.coffee +9 -8
- data/lib/written/app/assets/javascripts/written/core/{extensions/string.coffee → html.coffee} +3 -2
- data/lib/written/app/assets/javascripts/written/core/parsers.coffee +179 -0
- data/lib/written/app/assets/javascripts/written/parsers/block/code.coffee +11 -28
- data/lib/written/app/assets/javascripts/written/parsers/block/heading.coffee +37 -39
- data/lib/written/app/assets/javascripts/written/parsers/block/image.coffee +15 -39
- data/lib/written/app/assets/javascripts/written/parsers/block/olist.coffee +23 -20
- data/lib/written/app/assets/javascripts/written/parsers/block/paragraph.coffee +14 -34
- data/lib/written/app/assets/javascripts/written/parsers/block/quote.coffee +19 -38
- data/lib/written/app/assets/javascripts/written/parsers/block/ulist.coffee +20 -18
- data/lib/written/app/assets/javascripts/written/parsers/inline/bold.coffee +23 -0
- data/lib/written/app/assets/javascripts/written/parsers/inline/code.coffee +8 -27
- data/lib/written/app/assets/javascripts/written/parsers/inline/italic.coffee +8 -28
- data/lib/written/app/assets/javascripts/written/parsers/inline/link.coffee +8 -27
- data/lib/written/app/assets/stylesheets/written.scss +4 -0
- data/lib/written/version.rb +1 -1
- data/package.json +25 -0
- data/test/javascript/parsers/block/code.js +58 -0
- data/test/javascript/parsers/block/header.js +40 -0
- data/test/javascript/parsers/block/image.js +39 -0
- data/test/javascript/parsers/block/olist.js +41 -0
- data/test/javascript/parsers/block/paragraph.js +42 -0
- data/test/javascript/parsers/block/quote.js +41 -0
- data/test/javascript/parsers/block/ulist.js +40 -0
- data/test/javascript/parsers/inline/code.js +41 -0
- data/test/javascript/parsers/inline/italic.js +42 -0
- data/test/javascript/parsers/inline/link.js +42 -0
- data/test/javascript/parsers/inline/strong.js +41 -0
- data/test/server/app/assets/javascripts/application.coffee +8 -9
- data/test/server/app/assets/javascripts/secret.coffee +6 -0
- data/test/server/app/views/posts/show.html.erb +4 -4
- metadata +21 -12
- data/lib/written/app/assets/javascripts/written/parsers/inline/strong.coffee +0 -43
- data/lib/written/app/assets/javascripts/written/parsers/parsers.coffee +0 -95
- data/test/javascript/assertions/assert.coffee +0 -3
- data/test/javascript/polyfills.coffee +0 -2
- data/test/javascript/polyfills/HTMLULListElement.coffee +0 -0
- data/test/javascript/polyfills/Text.coffee +0 -0
- data/test/javascript/runner.coffee +0 -46
- data/test/javascript/tests/initialization.coffee +0 -16
- data/test/javascript/tests/parsing.coffee +0 -9
@@ -1,4 +1,5 @@
|
|
1
|
-
|
1
|
+
RULE = /^(>\s)(.*)/i
|
2
|
+
class Quote extends Written.Parsers.Block
|
2
3
|
multiline: true
|
3
4
|
|
4
5
|
constructor: (match) ->
|
@@ -6,18 +7,18 @@ class Quote
|
|
6
7
|
@opened = true
|
7
8
|
|
8
9
|
accepts: (text) ->
|
9
|
-
@opened =
|
10
|
+
@opened = RULE.test(text)
|
10
11
|
|
11
12
|
append: (text) ->
|
12
|
-
@matches.push(
|
13
|
+
@matches.push(RULE.exec(text))
|
13
14
|
|
14
|
-
|
15
|
+
outerText: ->
|
15
16
|
lines = @matches.map (match) ->
|
16
17
|
match[0]
|
17
18
|
|
18
19
|
lines.join('\n')
|
19
20
|
|
20
|
-
|
21
|
+
innerText: =>
|
21
22
|
lines = @matches.map (match) ->
|
22
23
|
match[2]
|
23
24
|
|
@@ -26,15 +27,15 @@ class Quote
|
|
26
27
|
equals: (current, rendered) ->
|
27
28
|
current.outerHTML == rendered.outerHTML
|
28
29
|
|
29
|
-
|
30
|
-
node = "<blockquote></blockquote>"
|
30
|
+
toEditor: =>
|
31
|
+
node = Written.toHTML("<blockquote></blockquote>")
|
31
32
|
for line, index in @content
|
32
|
-
p = "<p>"
|
33
|
+
p = Written.toHTML("<p>")
|
33
34
|
p.appendChild(document.createTextNode(@matches[index][1]))
|
34
35
|
|
35
36
|
for text in line
|
36
|
-
if text.
|
37
|
-
p.appendChild(text.
|
37
|
+
if text.toEditor?
|
38
|
+
p.appendChild(text.toEditor())
|
38
39
|
else
|
39
40
|
p.appendChild(document.createTextNode(text.toString()))
|
40
41
|
|
@@ -44,14 +45,14 @@ class Quote
|
|
44
45
|
|
45
46
|
node
|
46
47
|
|
47
|
-
|
48
|
-
node = "<blockquote></blockquote>"
|
48
|
+
toHTML: =>
|
49
|
+
node = Written.toHTML("<blockquote></blockquote>")
|
49
50
|
for line, index in @content
|
50
|
-
p = "<p>"
|
51
|
+
p = Written.toHTML("<p>")
|
51
52
|
|
52
53
|
for text in line
|
53
|
-
if text.
|
54
|
-
p.appendChild(text.
|
54
|
+
if text.toHTML?
|
55
|
+
p.appendChild(text.toHTML())
|
55
56
|
else
|
56
57
|
p.appendChild(document.createTextNode(text.toString()))
|
57
58
|
|
@@ -62,30 +63,10 @@ class Quote
|
|
62
63
|
node
|
63
64
|
|
64
65
|
|
65
|
-
Quote.rule = /^(>\s)(.*)/i
|
66
|
-
|
67
66
|
Written.Parsers.register {
|
68
67
|
parser: Quote
|
69
|
-
|
68
|
+
name: 'quote'
|
69
|
+
nodes: ['blockquote']
|
70
70
|
type: 'block'
|
71
|
-
|
72
|
-
range = document.createRange()
|
73
|
-
|
74
|
-
if !node.firstChild?
|
75
|
-
range.setStart(this, 0)
|
76
|
-
else
|
77
|
-
while walker.nextNode()
|
78
|
-
if walker.currentNode.length < offset
|
79
|
-
offset -= walker.currentNode.length
|
80
|
-
continue
|
81
|
-
|
82
|
-
range.setStart(walker.currentNode, offset)
|
83
|
-
break
|
84
|
-
|
85
|
-
range.collapse(true)
|
86
|
-
range
|
87
|
-
|
88
|
-
toString: (node) ->
|
89
|
-
node.textContent
|
90
|
-
|
71
|
+
rule: RULE
|
91
72
|
}
|
@@ -1,41 +1,43 @@
|
|
1
|
-
|
1
|
+
RULE = /^(-\s)(.*)/i
|
2
|
+
|
3
|
+
class UList extends Written.Parsers.Block
|
2
4
|
multiline: true
|
3
5
|
|
4
6
|
constructor: (match) ->
|
5
7
|
@matches = [match]
|
6
8
|
|
7
9
|
accepts: (text) ->
|
8
|
-
@opened =
|
10
|
+
@opened = RULE.test(text)
|
9
11
|
|
10
12
|
@opened
|
11
13
|
|
12
|
-
|
14
|
+
outerText: ->
|
13
15
|
texts = @matches.map (match) ->
|
14
16
|
match[0]
|
15
17
|
|
16
18
|
texts.join('\n')
|
17
19
|
|
18
|
-
|
20
|
+
innerText: ->
|
19
21
|
texts = @matches.map (match) ->
|
20
22
|
match[2]
|
21
23
|
|
22
24
|
texts.join('\n')
|
23
25
|
|
24
26
|
append: (text) ->
|
25
|
-
@matches.push(
|
27
|
+
@matches.push(RULE.exec(text))
|
26
28
|
|
27
29
|
equals: (current, rendered) ->
|
28
30
|
current.outerHTML == rendered.outerHTML
|
29
31
|
|
30
|
-
|
31
|
-
node = "<ul></ul>"
|
32
|
+
toEditor: =>
|
33
|
+
node = Written.toHTML("<ul></ul>")
|
32
34
|
for line, index in @content
|
33
|
-
li = "<li>"
|
35
|
+
li = Written.toHTML("<li>")
|
34
36
|
li.appendChild(document.createTextNode(@matches[index][1]))
|
35
37
|
|
36
38
|
for text in line
|
37
|
-
if text.
|
38
|
-
li.appendChild(text.
|
39
|
+
if text.toEditor?
|
40
|
+
li.appendChild(text.toEditor())
|
39
41
|
else
|
40
42
|
li.appendChild(document.createTextNode(text.toString()))
|
41
43
|
|
@@ -43,14 +45,14 @@ class UList
|
|
43
45
|
|
44
46
|
node
|
45
47
|
|
46
|
-
|
47
|
-
node = "<ul></ul>"
|
48
|
+
toHTML: =>
|
49
|
+
node = Written.toHTML("<ul></ul>")
|
48
50
|
for line, index in @content
|
49
|
-
li = "<li>"
|
51
|
+
li = Written.toHTML("<li>")
|
50
52
|
|
51
53
|
for text in line
|
52
|
-
if text.
|
53
|
-
li.appendChild(text.
|
54
|
+
if text.toHTML?
|
55
|
+
li.appendChild(text.toHTML())
|
54
56
|
else
|
55
57
|
li.appendChild(document.createTextNode(text.toString()))
|
56
58
|
|
@@ -58,12 +60,12 @@ class UList
|
|
58
60
|
|
59
61
|
node
|
60
62
|
|
61
|
-
UList.rule = /^(-\s)(.*)/i
|
62
|
-
|
63
63
|
Written.Parsers.register {
|
64
64
|
parser: UList
|
65
|
-
|
65
|
+
name: 'ulist'
|
66
|
+
nodes: ['ul']
|
66
67
|
type: 'block'
|
68
|
+
rule: RULE
|
67
69
|
getRange: (node, offset, walker) ->
|
68
70
|
range = document.createRange()
|
69
71
|
if !node.firstChild?
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class Bold extends Written.Parsers.Inline
|
2
|
+
constructor: (match) ->
|
3
|
+
@match = match
|
4
|
+
|
5
|
+
index: =>
|
6
|
+
@match.index
|
7
|
+
|
8
|
+
length: =>
|
9
|
+
@match[0].length
|
10
|
+
|
11
|
+
toEditor: =>
|
12
|
+
Written.toHTML("<strong>#{@match[0]}</strong>")
|
13
|
+
|
14
|
+
toHTML: =>
|
15
|
+
Written.toHTML("<strong>#{@match[3]}</strong>")
|
16
|
+
|
17
|
+
Written.Parsers.register {
|
18
|
+
parser: Bold
|
19
|
+
name: 'bold'
|
20
|
+
nodes: ['strong']
|
21
|
+
type: 'inline'
|
22
|
+
rule: /((\*{1})([^\*]+)(\*{1}))/gi
|
23
|
+
}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
class Code
|
1
|
+
class Code extends Written.Parsers.Inline
|
2
2
|
constructor: (match) ->
|
3
3
|
@match = match
|
4
4
|
|
@@ -8,8 +8,8 @@ class Code
|
|
8
8
|
length: =>
|
9
9
|
@match[0].length
|
10
10
|
|
11
|
-
|
12
|
-
node = "<code>#{this.match[0]}</code>"
|
11
|
+
toEditor: =>
|
12
|
+
node = Written.toHTML("<code>#{this.match[0]}</code>")
|
13
13
|
if @match[3]?
|
14
14
|
node.classList.add("language-#{@match[3]}")
|
15
15
|
|
@@ -17,40 +17,21 @@ class Code
|
|
17
17
|
@highlight(node)
|
18
18
|
node
|
19
19
|
|
20
|
-
|
21
|
-
node = "<code>#{this.match[4]}</code>"
|
20
|
+
toHTML: =>
|
21
|
+
node = Written.toHTML("<code>#{this.match[4]}</code>")
|
22
22
|
if @match[3]?
|
23
23
|
node.classList.add("language-#{@match[3]}")
|
24
24
|
|
25
25
|
node
|
26
26
|
|
27
27
|
|
28
|
-
Code.rule = /((~{3})([a-z]+)?)\s(.+)?(~{3})/gi
|
29
28
|
|
30
29
|
Written.Parsers.register {
|
31
30
|
parser: Code
|
32
|
-
|
31
|
+
name: 'code'
|
32
|
+
nodes: ['code']
|
33
33
|
type: 'inline'
|
34
|
-
|
35
|
-
range = document.createRange()
|
36
|
-
|
37
|
-
if !@firstChild?
|
38
|
-
range.setStart(this, 0)
|
39
|
-
else
|
40
|
-
while walker.nextNode()
|
41
|
-
if walker.currentNode.length < offset
|
42
|
-
offset -= walker.currentNode.length
|
43
|
-
continue
|
44
|
-
|
45
|
-
range.setStart(walker.currentNode, offset)
|
46
|
-
break
|
47
|
-
|
48
|
-
range.collapse(true)
|
49
|
-
range
|
50
|
-
|
51
|
-
toString: (node) ->
|
52
|
-
node.textContent
|
53
|
-
|
34
|
+
rule: /((~{3})([a-z]+)?)\s(.+)?(~{3})/gi
|
54
35
|
highlightWith: (callback) ->
|
55
36
|
Code.prototype.highlight = callback
|
56
37
|
}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
class Italic
|
1
|
+
class Italic extends Written.Parsers.Inline
|
2
2
|
constructor: (match) ->
|
3
3
|
@match = match
|
4
4
|
|
@@ -8,36 +8,16 @@ class Italic
|
|
8
8
|
length: =>
|
9
9
|
@match[0].length
|
10
10
|
|
11
|
-
|
12
|
-
"<em>#{this.match[1]}</em>"
|
13
|
-
|
14
|
-
html: ->
|
15
|
-
"<em>#{this.match[3]}</em>".toHTML()
|
16
|
-
|
17
|
-
Italic.rule = /((_{1})([^_]+)(_{1}))/gi
|
11
|
+
toEditor: =>
|
12
|
+
Written.toHTML("<em>#{this.match[1]}</em>")
|
18
13
|
|
14
|
+
toHTML: ->
|
15
|
+
Written.toHTML("<em>#{this.match[3]}</em>")
|
19
16
|
|
20
17
|
Written.Parsers.register {
|
21
18
|
parser: Italic
|
22
|
-
|
19
|
+
name: 'italic'
|
20
|
+
nodes: ['em']
|
23
21
|
type: 'inline'
|
24
|
-
|
25
|
-
range = document.createRange()
|
26
|
-
|
27
|
-
if !node.firstChild?
|
28
|
-
range.setStart(this, 0)
|
29
|
-
else
|
30
|
-
while walker.nextNode()
|
31
|
-
if walker.currentNode.length < offset
|
32
|
-
offset -= walker.currentNode.length
|
33
|
-
continue
|
34
|
-
|
35
|
-
range.setStart(walker.currentNode, offset)
|
36
|
-
break
|
37
|
-
|
38
|
-
range.collapse(true)
|
39
|
-
range
|
40
|
-
|
41
|
-
toString: (node) ->
|
42
|
-
node.textContent
|
22
|
+
rule: /((_{1})([^_]+)(_{1}))/gi
|
43
23
|
}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
class Link
|
1
|
+
class Link extends Written.Parsers.Inline
|
2
2
|
constructor: (match) ->
|
3
3
|
@match = match
|
4
4
|
|
@@ -8,36 +8,17 @@ class Link
|
|
8
8
|
length: =>
|
9
9
|
@match[0].length
|
10
10
|
|
11
|
-
|
12
|
-
"<a
|
11
|
+
toEditor: =>
|
12
|
+
Written.toHTML("<a><strong>#{@match[1]}</strong>#{@match[3]}</a>")
|
13
13
|
|
14
|
-
|
15
|
-
"<a href='#{@match[4]}'>#{@match[2]}</a>"
|
16
|
-
|
17
|
-
Link.rule = /!{0}(\[([^\]]+)\])(\(([^\)]+)\))/gi
|
14
|
+
toHTML: =>
|
15
|
+
Written.toHTML("<a href='#{@match[4]}'>#{@match[2]}</a>")
|
18
16
|
|
19
17
|
|
20
18
|
Written.Parsers.register {
|
21
19
|
parser: Link
|
22
|
-
|
20
|
+
name: 'link'
|
21
|
+
nodes: ['a']
|
23
22
|
type: 'inline'
|
24
|
-
|
25
|
-
range = document.createRange()
|
26
|
-
|
27
|
-
if !node.firstChild?
|
28
|
-
range.setStart(this, 0)
|
29
|
-
else
|
30
|
-
while walker.nextNode()
|
31
|
-
if walker.currentNode.length < offset
|
32
|
-
offset -= walker.currentNode.length
|
33
|
-
continue
|
34
|
-
|
35
|
-
range.setStart(walker.currentNode, offset)
|
36
|
-
break
|
37
|
-
|
38
|
-
range.collapse(true)
|
39
|
-
range
|
40
|
-
|
41
|
-
toString: (node) ->
|
42
|
-
node.textContent
|
23
|
+
rule: /!{0}(\[([^\]]+)\])(\(([^\)]+)\))/gi
|
43
24
|
}
|
data/lib/written/version.rb
CHANGED
data/package.json
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
{
|
2
|
+
"name": "written",
|
3
|
+
"description": "Markdown Editor",
|
4
|
+
"version": "0.0.1",
|
5
|
+
"private": true,
|
6
|
+
"scripts": {
|
7
|
+
"test": "jest"
|
8
|
+
},
|
9
|
+
"devDependencies": {
|
10
|
+
"babel-jest": "^12.1.0",
|
11
|
+
"babel-polyfill": "^6.9.0",
|
12
|
+
"babel-preset-es2015": "^6.9.0",
|
13
|
+
"coffee-script": "^1.10.0",
|
14
|
+
"jest": "^12.1.1",
|
15
|
+
"jest-cli": "^12.1.0"
|
16
|
+
},
|
17
|
+
"jest": {
|
18
|
+
"testDirectoryName": "test/javascript",
|
19
|
+
"scriptPreprocessor": "<rootDir>/compile-coffee",
|
20
|
+
"moduleFileExtensions": [
|
21
|
+
"coffee",
|
22
|
+
"js"
|
23
|
+
]
|
24
|
+
}
|
25
|
+
}
|
@@ -0,0 +1,58 @@
|
|
1
|
+
let struct = undefined;
|
2
|
+
|
3
|
+
window.Written = {
|
4
|
+
Parsers: {
|
5
|
+
Block: {},
|
6
|
+
register: function(s) {
|
7
|
+
struct = s
|
8
|
+
}
|
9
|
+
}
|
10
|
+
}
|
11
|
+
|
12
|
+
require('../../../../lib/written/app/assets/javascripts/written/core/html')
|
13
|
+
require('../../../../lib/written/app/assets/javascripts/written/parsers/block/code')
|
14
|
+
|
15
|
+
describe('code', () => {
|
16
|
+
it('detects a match', () => {
|
17
|
+
let match = struct.rule.exec('~~~')
|
18
|
+
expect(match).not.toBeNull()
|
19
|
+
})
|
20
|
+
|
21
|
+
it('can add lines until it finds the closing tag', () => {
|
22
|
+
let match = struct.rule.exec('~~~')
|
23
|
+
let parser = new struct.parser(match)
|
24
|
+
|
25
|
+
expect(match).not.toBeNull()
|
26
|
+
expect(parser.accepts('let var = true')).toBeTruthy()
|
27
|
+
parser.append('let var = true')
|
28
|
+
|
29
|
+
expect(parser.accepts('~~~')).toBeTruthy()
|
30
|
+
parser.append('~~~')
|
31
|
+
|
32
|
+
expect(parser.opened).toBeFalsy()
|
33
|
+
})
|
34
|
+
|
35
|
+
it('returns the content for the editor', () => {
|
36
|
+
let match = struct.rule.exec('~~~')
|
37
|
+
let html = Written.toHTML('<pre><code>~~~\nlet var = true\n~~~</code></pre>')
|
38
|
+
|
39
|
+
let parser = new struct.parser(match)
|
40
|
+
parser.append('let var = true')
|
41
|
+
parser.append('~~~')
|
42
|
+
|
43
|
+
expect(parser.opened).toBeFalsy()
|
44
|
+
expect(parser.toEditor().outerHTML).toBe(html.outerHTML)
|
45
|
+
})
|
46
|
+
|
47
|
+
it('HTML content should not include the markdown syntax', () => {
|
48
|
+
let match = struct.rule.exec('~~~')
|
49
|
+
let html = Written.toHTML('<pre><code>\nlet var = true\n</code></pre>')
|
50
|
+
|
51
|
+
let parser = new struct.parser(match)
|
52
|
+
parser.append('let var = true')
|
53
|
+
parser.append('~~~')
|
54
|
+
|
55
|
+
expect(parser.opened).toBeFalsy()
|
56
|
+
expect(parser.toHTML().outerHTML).toBe(html.outerHTML)
|
57
|
+
})
|
58
|
+
})
|