graphdown 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7b329ba5609ea9ef8ce2ac37f20d4281f03da537
4
- data.tar.gz: ef617d20ca0ccac07840e32b55d15544b6a0eba2
3
+ metadata.gz: 25f4809d61a0512f86a702e492528795fbc3951d
4
+ data.tar.gz: 89940ed3e506736155d2a841d8e4f5abbcd38682
5
5
  SHA512:
6
- metadata.gz: fbc79e190bde04380d98dd47db187e384c88ac38dc9d656f440283dfb3c09b74cca17dd7e3f4e2c4c7d42a6a07856e0e4a7a7b06b891815b136a3ce2e04a47fd
7
- data.tar.gz: 1fee7608cea1f178ec37a8cf6f592d3846be1718b1e26abba75d468acdb095401b17098ce1ec2f2ff51ab0e9f5f3e282c98011812f9ec713fae76ca4923d1146
6
+ metadata.gz: d67dab59ae625d6dab02cc780f59b2326d3772d594daf24640c58ca8f2cd8b75a149a56da4167ae5c4709ed2620a29f46607785667a6aad53d3b015d1dd0a278
7
+ data.tar.gz: b5bfa36722e6e9cd9e3fd907bf76c7aebfb8481fbd9d669bda6c8ad6a327729f15169861bcb8ca0161a46430e4bd8c4230fb3bad5aa372b9977e434e1addd42d
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - "1.9.3"
4
+ - "2.0.0"
5
+ - "2.1.0"
6
+ script: bundle exec rspec
data/README.md CHANGED
@@ -8,11 +8,17 @@ Markdown extension for embedding graphs.
8
8
  $ gem install graphdown
9
9
  ```
10
10
 
11
- ## Requirements
11
+ ## Usage
12
12
 
13
- - [Graphviz](http://www.graphviz.org/)
13
+ ### CLI
14
14
 
15
- ## Usage
15
+ ```sh
16
+ $ graphdown sample.md
17
+ ```
18
+
19
+ This command generates `sample.html`, which is parsed into HTML with graphs in format of SVG.
20
+
21
+ ### Redcarpet
16
22
 
17
23
  ```rb
18
24
  require "redcarpet"
@@ -20,34 +26,42 @@ require "graphdown"
20
26
 
21
27
  class BaseRenderer < Redcarpet::Render::HTML
22
28
  include Graphdown::Renderable
23
- # include other extensions
24
29
  end
25
30
 
26
31
  markdown = Redcarpet::Markdown.new(BaseRenderer, fenced_code_blocks: true)
27
- markdown.render(content)
28
32
  ```
29
33
 
30
- ## Example
34
+ ## Graph notation
31
35
 
32
- ```md
33
- # Views transition
36
+ Graphdown extends following notations for graphs.
34
37
 
35
- \```dot
36
- digraph sample {
37
- A [label = "index.html"];
38
- B [label = "show.html"];
39
- C [label = "new.html"];
38
+ - `[label name]`: Node named "label name"
39
+ - `[label A], [label B], ...`: Multiple nodes
40
+ - `->`: Unidirectional edge
41
+ - `<->`: Bidirectional edge
40
42
 
41
- A -> B [dir = both];
42
- A -> C;
43
- C -> A [label = "redirect"];
44
- }
45
- \```
43
+ Graphdown parses these notations into graph images in SVG format.
46
44
 
47
- - Users visit show.html from index.html.
48
- - Users visit index.html from show.html.
49
- - Users visit new.html from index.html.
50
- - Users are redirected to index.html from new.html.
51
- ```
45
+ ### Examples
46
+
47
+ <pre>
48
+ # Servers arrangement
49
+
50
+ [Load balancer] -> [Web server 1], [Web server 2], [Web server 3] -> [DB server], [Cache server]
51
+ </pre>
52
+
53
+ ![Servers arrangement](examples/servers_arrangement.png)
54
+
55
+ <pre>
56
+ # Path to naoty/graphdown on GitHub
57
+
58
+ [/] -> [/naoty] -> [/naoty/graphdown]
59
+ [/] -> [/search] -> [/naoty/graphdown]
60
+ [/] -> [/notifications] -> [/naoty/graphdown]
61
+ </pre>
62
+
63
+ ![Path to graphdown](examples/path_to_graphdown.png)
64
+
65
+ ## References
52
66
 
53
- Graphdown parses block codes which language is 'dot' into img tags, which load graph PNG image. The image is generated by Graphviz.
67
+ - Kazuo Misue, Kouzou Sugiyama, On Automatic Drowing of Compound Graphs for Computer Aided Diagrammatical Thinking, Journal of Information Processing Society of Japan, 1989, Vol.30, No.10, p1324-1334.
data/bin/graphdown ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "graphdown"
4
+ Graphdown::CLI.start(ARGV)
@@ -0,0 +1,31 @@
1
+ <h1>Path to naoty/graphdown on GitHub</h1>
2
+ <svg width="395" height="275" xmlns="http://www.w3.org/2000/svg">
3
+ <g fill="none" stroke="black">
4
+ <rect x="10" y="10" width="33" height="35"/>
5
+ <rect x="10" y="95" width="78" height="35"/>
6
+ <rect x="10" y="180" width="168" height="35"/>
7
+ <rect x="108" y="95" width="87" height="35"/>
8
+ <rect x="215" y="95" width="150" height="35"/>
9
+ </g>
10
+ <g font-size="18" font-family="monospace" text-anchor="middle">
11
+ <text x="26" y="37">/</text>
12
+ <text x="49" y="122">/naoty</text>
13
+ <text x="94" y="207">/naoty/graphdown</text>
14
+ <text x="151" y="122">/search</text>
15
+ <text x="290" y="122">/notifications</text>
16
+ </g>
17
+ <g stroke="black" stroke-width="2">
18
+ <path d="M 18.25 45 L 49.0 95" fill="none"/>
19
+ <path d="M 49.0 95 L 40.204218667115356 90.24245538706381 L 48.72226382799822 85.00385761312086 Z" fill="black"/>
20
+ <path d="M 26.5 45 L 151.5 95" fill="none"/>
21
+ <path d="M 151.5 95 L 141.602202606946 96.42604584991292 L 145.31610937048706 87.14127894106032 Z" fill="black"/>
22
+ <path d="M 34.75 45 L 290.0 95" fill="none"/>
23
+ <path d="M 290.0 95 L 280.54010074624506 98.24196022628374 L 282.46243045910205 88.42846704214861 Z" fill="black"/>
24
+ <path d="M 49.0 130 L 52.0 180" fill="none"/>
25
+ <path d="M 52.0 180 L 46.49029332005865 171.6547539100989 L 56.47234177471645 171.05583100281942 Z" fill="black"/>
26
+ <path d="M 151.5 130 L 94.0 180" fill="none"/>
27
+ <path d="M 94.0 180 L 97.25418194567223 170.54429802370757 L 103.81596909492009 178.0903532453426 Z" fill="black"/>
28
+ <path d="M 290.0 130 L 136.0 180" fill="none"/>
29
+ <path d="M 136.0 180 L 142.69294863675958 172.57003105351723 L 145.78101617536336 182.08127907241686 Z" fill="black"/>
30
+ </g>
31
+ </svg>
@@ -0,0 +1,5 @@
1
+ # Path to naoty/graphdown on GitHub
2
+
3
+ [/] -> [/naoty] -> [/naoty/graphdown]
4
+ [/] -> [/search] -> [/naoty/graphdown]
5
+ [/] -> [/notifications] -> [/naoty/graphdown]
Binary file
@@ -0,0 +1,39 @@
1
+ <h1>Servers arrangement</h1>
2
+ <svg width="476" height="275" xmlns="http://www.w3.org/2000/svg">
3
+ <g fill="none" stroke="black">
4
+ <rect x="10" y="10" width="141" height="35"/>
5
+ <rect x="10" y="95" width="132" height="35"/>
6
+ <rect x="162" y="95" width="132" height="35"/>
7
+ <rect x="314" y="95" width="132" height="35"/>
8
+ <rect x="10" y="180" width="105" height="35"/>
9
+ <rect x="135" y="180" width="132" height="35"/>
10
+ </g>
11
+ <g font-size="18" font-family="monospace" text-anchor="middle">
12
+ <text x="80" y="37">Load balancer</text>
13
+ <text x="76" y="122">Web server 1</text>
14
+ <text x="228" y="122">Web server 2</text>
15
+ <text x="380" y="122">Web server 3</text>
16
+ <text x="62" y="207">DB server</text>
17
+ <text x="201" y="207">Cache server</text>
18
+ </g>
19
+ <g stroke="black" stroke-width="2">
20
+ <path d="M 45.25 45 L 76.0 95" fill="none"/>
21
+ <path d="M 76.0 95 L 67.20421866711536 90.24245538706381 L 75.72226382799822 85.00385761312086 Z" fill="black"/>
22
+ <path d="M 80.5 45 L 228.0 95" fill="none"/>
23
+ <path d="M 228.0 95 L 218.1929724878952 96.95504766612466 L 221.4033652994742 87.48438887196666 Z" fill="black"/>
24
+ <path d="M 115.75 45 L 380.0 95" fill="none"/>
25
+ <path d="M 380.0 95 L 370.56115218759226 98.30274915399355 L 372.4203114241102 88.4770925889965 Z" fill="black"/>
26
+ <path d="M 54.0 130 L 36.25 180" fill="none"/>
27
+ <path d="M 36.25 180 L 34.435344142727494 170.16602704296648 L 43.85914247228395 173.51147544995902 Z" fill="black"/>
28
+ <path d="M 98.0 130 L 168.0 180" fill="none"/>
29
+ <path d="M 168.0 180 L 158.04667045165723 179.03499694192848 L 163.85905238884823 170.8976622298611 Z" fill="black"/>
30
+ <path d="M 206.0 130 L 62.5 180" fill="none"/>
31
+ <path d="M 62.5 180 L 69.0328881004204 172.42891202881742 L 72.3231985675411 181.87210306945383 Z" fill="black"/>
32
+ <path d="M 250.0 130 L 201.0 180" fill="none"/>
33
+ <path d="M 201.0 180 L 203.4904951358704 170.3150924641375 L 210.63262352729546 177.3143782877341 Z" fill="black"/>
34
+ <path d="M 358.0 130 L 88.75 180" fill="none"/>
35
+ <path d="M 88.75 180 L 96.3517865603404 173.50285900637604 L 98.17758243261781 183.33476977858993 Z" fill="black"/>
36
+ <path d="M 402.0 130 L 234.0 180" fill="none"/>
37
+ <path d="M 234.0 180 L 240.87417050853045 172.7373710118408 L 243.72670645627238 182.3218917962536 Z" fill="black"/>
38
+ </g>
39
+ </svg>
@@ -0,0 +1,3 @@
1
+ # Servers arrangement
2
+
3
+ [Load balancer] -> [Web server 1], [Web server 2], [Web server 3] -> [DB server], [Cache server]
Binary file
data/graphdown.gemspec CHANGED
@@ -1,9 +1,11 @@
1
1
  lib = File.expand_path("../lib", __FILE__)
2
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
3
 
4
+ require "graphdown/version"
5
+
4
6
  Gem::Specification.new do |spec|
5
7
  spec.name = "graphdown"
6
- spec.version = "0.0.1"
8
+ spec.version = Graphdown::VERSION
7
9
  spec.authors = ["Naoto Kaneko"]
8
10
  spec.email = ["naoty.k@gmail.com"]
9
11
  spec.summary = %q{Markdown extension for embedding graphs.}
@@ -15,6 +17,8 @@ Gem::Specification.new do |spec|
15
17
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
16
18
  spec.require_paths = ["lib"]
17
19
 
20
+ spec.add_dependency "svgen"
21
+ spec.add_dependency "thor", "~> 0.18.1"
18
22
  spec.add_dependency "redcarpet", "~> 3.0.0"
19
23
  spec.add_development_dependency "bundler", "~> 1.5"
20
24
  spec.add_development_dependency "rake"
@@ -0,0 +1,47 @@
1
+ require "redcarpet"
2
+ require "pathname"
3
+ require "optparse"
4
+
5
+ module Graphdown
6
+ class CLI
7
+ def self.start(args)
8
+ method_name = args.shift
9
+ new.send(method_name, *args)
10
+ end
11
+
12
+ def initialize
13
+ @options = {}
14
+ @option_parser = OptionParser.new
15
+ end
16
+
17
+ def parse(name, *args)
18
+ @option_parser.on("-o", "--output FILENAME") { |filename| @options[:output] = filename }
19
+ @option_parser.parse!(args)
20
+
21
+ markdown_path = Pathname.new(name).expand_path
22
+ output_filename = @options[:output] || File.basename(name, ".md") + ".html"
23
+ html_path = markdown_path.dirname.join(output_filename)
24
+
25
+ renderer = Class.new(Redcarpet::Render::HTML)
26
+ renderer.send(:include, Graphdown::Renderable)
27
+ markdown = Redcarpet::Markdown.new(renderer)
28
+
29
+ html = markdown.render(markdown_path.read)
30
+ html_path.open("wb") { |file| file.write(html) }
31
+ end
32
+
33
+ def method_missing(name, *args)
34
+ case name.to_s
35
+ when /\.md$/
36
+ parse(name.to_s, *args)
37
+ when "-h", "--help"
38
+ args << "--help"
39
+ parse(name.to_s, *args)
40
+ when "-v", "--version"
41
+ puts Graphdown::VERSION
42
+ else
43
+ super
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,118 @@
1
+ require "matrix"
2
+
3
+ module Graphdown
4
+ class Edge
5
+ attr_accessor :origin, :target
6
+ attr_reader :direction
7
+
8
+ DIRECTION = [:forward, :backward, :two_way].freeze
9
+ ARROW_SIDE_LENGTH = 10
10
+
11
+ def initialize(origin_node, target_node, direction = :forward)
12
+ @origin_node = origin_node
13
+ @target_node = target_node
14
+ @direction = DIRECTION.include?(direction) ? direction : :forward
15
+ @origin = Point.new
16
+ @target = Point.new
17
+ end
18
+
19
+ def forward?
20
+ @direction == :forward || @direction == :two_way
21
+ end
22
+
23
+ def backward?
24
+ @direction == :backward || @direction == :two_way
25
+ end
26
+
27
+ def parent
28
+ case @direction
29
+ when :forward then @origin_node
30
+ when :backward then @target_node
31
+ when :two_way then @origin_node
32
+ end
33
+ end
34
+
35
+ def child
36
+ case @direction
37
+ when :forward then @target_node
38
+ when :backward then @origin_node
39
+ when :two_way then @target_node
40
+ end
41
+ end
42
+
43
+ def line_d
44
+ "M #{@origin.x} #{@origin.y} L #{@target.x} #{@target.y}"
45
+ end
46
+
47
+ def arrow_d
48
+ ratio = ARROW_SIDE_LENGTH / length
49
+
50
+ v1 = Graphdown::Edge::Vector.new(@target.x - @origin.x, @target.y - @origin.y)
51
+ p1 = v1.scale(ratio).rotate(Math::PI / 6 * 5).point
52
+
53
+ v2 = Graphdown::Edge::Vector.new(@target.x - @origin.x, @target.y - @origin.y)
54
+ p2 = v2.scale(ratio).rotate(-Math::PI / 6 * 5).point
55
+
56
+ p1.x += @target.x
57
+ p1.y += @target.y
58
+ p2.x += @target.x
59
+ p2.y += @target.y
60
+
61
+ "M #{@target.x} #{@target.y} L #{p1.x} #{p1.y} L #{p2.x} #{p2.y} Z"
62
+ end
63
+
64
+ def reverse_arrow_d
65
+ ratio = ARROW_SIDE_LENGTH / length
66
+
67
+ v1 = Graphdown::Edge::Vector.new(@origin.x - @target.x, @origin.y - @target.y)
68
+ p1 = v1.scale(ratio).rotate(Math::PI / 6 * 5).point
69
+
70
+ v2 = Graphdown::Edge::Vector.new(@origin.x - @target.x, @origin.y - @target.y)
71
+ p2 = v2.scale(ratio).rotate(-Math::PI / 6 * 5).point
72
+
73
+ p1.x += @origin.x
74
+ p1.y += @origin.y
75
+ p2.x += @origin.x
76
+ p2.y += @origin.y
77
+
78
+ "M #{@origin.x} #{@origin.y} L #{p1.x} #{p1.y} L #{p2.x} #{p2.y} Z"
79
+ end
80
+
81
+ private
82
+
83
+ def length
84
+ dx = @target.x - @origin.x
85
+ dy = @target.y - @origin.y
86
+ Math.sqrt(dx ** 2 + dy ** 2)
87
+ end
88
+
89
+ class Vector
90
+ def initialize(x, y)
91
+ @v = Matrix[[x, y]]
92
+ @matrices = []
93
+ end
94
+
95
+ def point
96
+ @matrices.each { |matrix| @v *= matrix }
97
+ x, y = @v.row(0).to_a
98
+ Point.new(x, y)
99
+ end
100
+
101
+ def scale(ratio)
102
+ unit_vector = @v.row(0).normalize
103
+ nx, ny = unit_vector.map(&:to_f).to_a
104
+ scaling_matrix = Matrix[[1 + (ratio - 1) * nx ** 2, (ratio - 1) * nx * ny],
105
+ [(ratio - 1) * nx * ny, 1 + (ratio - 1) * ny ** 2]]
106
+ @matrices << scaling_matrix
107
+ self
108
+ end
109
+
110
+ def rotate(radian)
111
+ rotation_matrix = Matrix[[Math.cos(radian), Math.sin(radian)],
112
+ [-Math.sin(radian), Math.cos(radian)]]
113
+ @matrices << rotation_matrix
114
+ self
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,104 @@
1
+ require "svgen"
2
+
3
+ module Graphdown
4
+ class Graph
5
+ attr_reader :nodes
6
+
7
+ PADDING = 10
8
+
9
+ def initialize
10
+ @nodes = []
11
+ @width = 0
12
+ @height = 0
13
+ end
14
+
15
+ def <<(node)
16
+ labels = @nodes.map(&:label)
17
+ @nodes << node unless labels.include?(node.label)
18
+ end
19
+
20
+ def find_node_by_label(label)
21
+ (@nodes || []).select { |node| node.label == label }.first
22
+ end
23
+
24
+ def layered_nodes
25
+ @layered_nodes ||= @nodes.sort_by(&:level).group_by(&:level).values
26
+ end
27
+
28
+ def layer_nodes
29
+ nodes_with_level = @nodes.select { |node| node.level >= 1 }
30
+ begin
31
+ diff_total = 0
32
+ nodes_with_level.each do |node|
33
+ node.children.each do |child|
34
+ if node.level >= child.level
35
+ child.level += node.level + 1
36
+ diff_total += node.level + 1
37
+ end
38
+ end
39
+ end
40
+ end while diff_total != 0
41
+ end
42
+
43
+ def layout_nodes
44
+ layer_widths = []
45
+ layer_offset = PADDING
46
+ layered_nodes.each do |nodes|
47
+ node_offset = PADDING
48
+ nodes.each do |node|
49
+ node.x = node_offset
50
+ node.y = layer_offset
51
+ node_offset += node.width + Node::MARGIN_RIGHT
52
+ end
53
+ layer_widths << node_offset + PADDING
54
+ layer_offset += nodes.map(&:height).max + Node::MARGIN_BOTTOM
55
+ end
56
+ @width = layer_widths.max
57
+ @height = layer_offset + PADDING
58
+ end
59
+
60
+ def layout_edges
61
+ @nodes.each do |node|
62
+ node.child_edges.each_with_index do |edge, index|
63
+ edge.origin.x = node.x + (node.width.to_f * (index + 1) / (node.child_edges.count + 1))
64
+ edge.origin.y = node.y + node.height
65
+ end
66
+
67
+ node.parent_edges.each_with_index do |edge, index|
68
+ edge.target.x = node.x + (node.width.to_f * (index + 1) / (node.parent_edges.count + 1))
69
+ edge.target.y = node.y
70
+ end
71
+ end
72
+ end
73
+
74
+ def to_svg
75
+ svg = SVGen::SVG.new(width: @width, height: @height) do |svg|
76
+ # nodes
77
+ svg.g(fill: "none", stroke: "black") do |g|
78
+ @nodes.each do |node|
79
+ g.rect(x: node.x, y: node.y, width: node.width, height: node.height)
80
+ end
81
+ end
82
+
83
+ # texts
84
+ svg.g("font-size" => Node::FONT_SIZE, "font-family" => Node::FONT_FAMILY, "text-anchor" => "middle") do |g|
85
+ @nodes.each do |node|
86
+ g.text(node.label, x: node.x + node.width / 2, y: node.y + Node::WORD_HEIGHT + Node::PADDING_TOP / 2)
87
+ end
88
+ end
89
+
90
+ # edges
91
+ svg.g(stroke: "black", "stroke-width" => 2) do |g|
92
+ @nodes.each do |node|
93
+ node.child_edges.each do |edge|
94
+ g.path(d: edge.line_d, fill: "none")
95
+ g.path(d: edge.arrow_d, fill: "black") if edge.forward?
96
+ g.path(d: edge.reverse_arrow_d, fill: "black") if edge.backward?
97
+ end
98
+ end
99
+ end
100
+ end
101
+ svg.generate
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,76 @@
1
+ module Graphdown
2
+ class Node
3
+ attr_accessor :x, :y, :level
4
+ attr_reader :label, :parent_edges, :child_edges
5
+
6
+ FONT_SIZE = 18
7
+ FONT_FAMILY = "monospace"
8
+
9
+ WORD_HEIGHT = 25
10
+ PADDING_TOP = 5
11
+ PADDING_LEFT = 10
12
+ MARGIN_RIGHT = 20
13
+ MARGIN_BOTTOM = 50
14
+
15
+ def initialize(label)
16
+ @x = 0
17
+ @y = 0
18
+ @level = 0
19
+ @label = label
20
+ @parent_edges = []
21
+ @child_edges = []
22
+ end
23
+
24
+ def width
25
+ text_width = 13 + (@label.length - 1) * 9
26
+ text_width + PADDING_LEFT * 2
27
+ end
28
+
29
+ def height
30
+ WORD_HEIGHT + PADDING_TOP * 2
31
+ end
32
+
33
+ def connect(child, direction = :forward)
34
+ child.level = 1
35
+
36
+ # Prevent closed path
37
+ direction = ancestors.include?(child) ? :backward : direction
38
+ edge = Edge.new(self, child, direction)
39
+ if direction == :backward
40
+ self.parent_edges << edge
41
+ child.child_edges << edge
42
+ else
43
+ self.child_edges << edge
44
+ child.parent_edges << edge
45
+ end
46
+ end
47
+
48
+ def parents
49
+ parent_edges.map(&:parent)
50
+ end
51
+
52
+ def ancestors
53
+ ancestors = []
54
+ ascend = ->(node) do
55
+ ancestors << node
56
+ node.parents.each { |parent| ascend.call(parent) }
57
+ end
58
+ parents.each { |parent| ascend.call(parent) }
59
+ ancestors
60
+ end
61
+
62
+ def children
63
+ child_edges.map(&:child)
64
+ end
65
+
66
+ def descendants
67
+ descendants = []
68
+ descend = ->(node) do
69
+ descendants << node
70
+ node.children.each { |child| descend.call(child) }
71
+ end
72
+ children.each { |child| descend.call(child) }
73
+ descendants
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,43 @@
1
+ module Graphdown
2
+ class Parser
3
+ attr_reader :graph
4
+
5
+ def initialize
6
+ @graph = Graph.new
7
+ end
8
+
9
+ def parse(text)
10
+ tokens = text.split(/(?<=[\]>])\s+/)
11
+ 2.step(tokens.size, 2) do |n|
12
+ # origins
13
+ origin_labels = tokens[n - 2].scan(/(?<=\[).+?(?=\])/)
14
+ origins = origin_labels.map do |label|
15
+ @graph.find_node_by_label(label) || Node.new(label)
16
+ end
17
+
18
+ # targets
19
+ target_labels = tokens[n].scan(/(?<=\[).+?(?=\])/)
20
+ targets = target_labels.map do |label|
21
+ @graph.find_node_by_label(label) || Node.new(label)
22
+ end
23
+
24
+ # edges
25
+ direction = (tokens[n - 1] == "<->") ? :two_way : :forward
26
+ origins.each do |origin|
27
+ @graph << origin
28
+ targets.each do |target|
29
+ origin.connect(target, direction)
30
+ @graph << target
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ def output
37
+ @graph.layer_nodes
38
+ @graph.layout_nodes
39
+ @graph.layout_edges
40
+ @graph.to_svg
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,10 @@
1
+ module Graphdown
2
+ class Point
3
+ attr_accessor :x, :y
4
+
5
+ def initialize(x = 0, y = 0)
6
+ @x = x
7
+ @y = y
8
+ end
9
+ end
10
+ end
@@ -1,38 +1,16 @@
1
- require "pathname"
2
- require "pry"
1
+ require "cgi"
3
2
 
4
3
  module Graphdown
5
4
  module Renderable
6
- if defined?(block_code)
7
- alias block_code_without_graphdown block_code
8
- else
9
- def block_code_without_graphdown(code, language)
10
- %(<p><code>#{code}</code></p>)
11
- end
12
- end
13
-
14
- def block_code_with_graphdown(code, language)
15
- if language == "dot"
16
- if code =~ /digraph\s+(\w+)\s+{/
17
- dot_path = Pathname.pwd.join("#{$1}.dot")
18
- dot_path.open("wb") { |f| f.write(code) }
19
- graph_path = Pathname.pwd.join("#{$1}.png")
20
- generate_graph(dot_path.to_s, graph_path.to_s)
21
- dot_path.delete
22
- %(<img src="#{graph_path.to_s}"/>)
23
- else
24
- %(<img src="#"/>)
25
- end
5
+ def paragraph(text)
6
+ if text =~ /^\s?\[.+\]\s?$/
7
+ parser = Parser.new
8
+ unescaped_text = CGI.unescape_html(text)
9
+ unescaped_text.split($/).each { |line| parser.parse(line) }
10
+ parser.output
26
11
  else
27
- block_code_without_graphdown(code, language)
12
+ %(<p>#{text}</p>)
28
13
  end
29
14
  end
30
- alias block_code block_code_with_graphdown
31
-
32
- private
33
-
34
- def generate_graph(source, output)
35
- system %(dot -Tpng -o #{output} #{source})
36
- end
37
15
  end
38
16
  end
@@ -0,0 +1,3 @@
1
+ module Graphdown
2
+ VERSION = "0.1.0"
3
+ end
data/lib/graphdown.rb CHANGED
@@ -1,5 +1,8 @@
1
- lib = File.dirname(__FILE__)
2
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
-
4
- require "redcarpet"
1
+ require "graphdown/version"
5
2
  require "graphdown/renderable"
3
+ require "graphdown/cli"
4
+ require "graphdown/parser"
5
+ require "graphdown/graph"
6
+ require "graphdown/node"
7
+ require "graphdown/edge"
8
+ require "graphdown/point"
@@ -1,18 +1 @@
1
- # Views transition
2
-
3
- ```dot
4
- digraph sample {
5
- A [label = "index.html"];
6
- B [label = "show.html"];
7
- C [label = "new.html"];
8
-
9
- A -> B [dir = both];
10
- A -> C;
11
- C -> A [label = "redirect"];
12
- }
13
- ```
14
-
15
- - Users visit show.html from index.html.
16
- - Users visit index.html from show.html.
17
- - Users visit new.html from index.html.
18
- - Users are redirected to index.html from new.html.
1
+ [a] -> [b] <-> [c]
@@ -0,0 +1,18 @@
1
+ <svg width="73" height="275" xmlns="http://www.w3.org/2000/svg">
2
+ <g fill="none" stroke="black">
3
+ <rect x="10" y="10" width="33" height="35"/>
4
+ <rect x="10" y="95" width="33" height="35"/>
5
+ <rect x="10" y="180" width="33" height="35"/>
6
+ </g>
7
+ <g font-size="18" font-family="monospace" text-anchor="middle">
8
+ <text x="26" y="37">a</text>
9
+ <text x="26" y="122">b</text>
10
+ <text x="26" y="207">c</text>
11
+ </g>
12
+ <g stroke="black" stroke-width="2">
13
+ <path d="M 26.5 45 L 26.5 95" fill="none"/>
14
+ <path d="M 26.5 95 L 31.5 85 L 21.5 85 Z" fill="black"/>
15
+ <path d="M 26.5 130 L 26.5 180" fill="none"/>
16
+ <path d="M 26.5 180 L 31.5 170 L 21.5 170 Z" fill="black"/>
17
+ </g>
18
+ </svg>
@@ -1,7 +1,3 @@
1
- # Routing
1
+ # Sample markdown
2
2
 
3
- ```rb
4
- SampleApp::Application.routes.draw do
5
- resources :contents
6
- end
7
- ```
3
+ Consectetur amet amet deserunt eos ducimus possimus rem nam esse eaque repudiandae? Nemo recusandae vero nesciunt maxime voluptatum accusamus laborum reiciendis soluta! Earum deleniti quaerat quia quisquam commodi dolores facere.
@@ -0,0 +1,32 @@
1
+ require "spec_helper"
2
+
3
+ describe Graphdown::Graph do
4
+ let(:graph) { Graphdown::Graph.new }
5
+ let(:a) { Graphdown::Node.new("a") }
6
+ let(:b) { Graphdown::Node.new("b") }
7
+ let(:c) { Graphdown::Node.new("c") }
8
+ let(:d) { Graphdown::Node.new("d") }
9
+ let(:e) { Graphdown::Node.new("e") }
10
+
11
+ before do
12
+ [a, b, c, d, e].each { |node| graph << node }
13
+ end
14
+
15
+ describe "#find_node_by_label" do
16
+ it "finds node" do
17
+ node = graph.find_node_by_label("a")
18
+ expect(node).to equal a
19
+ end
20
+ end
21
+
22
+ describe "#layered_nodes" do
23
+ it "returns layered nodes" do
24
+ e.connect(d)
25
+ e.connect(c)
26
+ d.connect(b)
27
+ b.connect(a)
28
+ graph.layer_nodes
29
+ expect(graph.layered_nodes).to match_array [[e], [d, c], [b], [a]]
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,35 @@
1
+ require "spec_helper"
2
+
3
+ describe Graphdown::Node do
4
+ let(:a) { Graphdown::Node.new("a") }
5
+ let(:b) { Graphdown::Node.new("b") }
6
+ let(:c) { Graphdown::Node.new("c") }
7
+
8
+ describe "#connect" do
9
+ it "connects node forward" do
10
+ a.connect(b)
11
+ expect(a.children).to be_include(b)
12
+ end
13
+
14
+ it "connects node backward" do
15
+ a.connect(b, :backward)
16
+ expect(a.parents).to be_include(b)
17
+ end
18
+
19
+ it "prevents closed path" do
20
+ a.connect(b)
21
+ b.connect(c)
22
+ c.connect(a)
23
+ expect(a.ancestors).to match_array []
24
+ expect(a.descendants).to match_array [b, c, c]
25
+ end
26
+ end
27
+
28
+ describe "#ancestors" do
29
+ it "returns parents of parents" do
30
+ a.connect(b)
31
+ b.connect(c)
32
+ expect(c.ancestors).to match_array [a, b]
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,33 @@
1
+ require "spec_helper"
2
+
3
+ describe Graphdown::Parser do
4
+ let(:parser) { Graphdown::Parser.new }
5
+
6
+ describe "#parse" do
7
+ let(:content) { "[a] -> [b] <-> [c]" }
8
+
9
+ before do
10
+ parser.parse(content)
11
+ end
12
+
13
+ it "parses text into nodes" do
14
+ labels = parser.graph.nodes.map(&:label)
15
+ expect(labels).to eq ["a", "b", "c"]
16
+ end
17
+
18
+ it "parses arrows into edges" do
19
+ edges = parser.graph.nodes.map(&:child_edges).flatten
20
+ directions = edges.map(&:direction)
21
+ expect(directions).to eq [:forward, :two_way]
22
+ end
23
+
24
+ context "when a node group is given" do
25
+ let(:content) { "[a] -> [b], [c], [d] -> [e]" }
26
+
27
+ it "parses text into nodes" do
28
+ labels = parser.graph.nodes.map(&:label)
29
+ expect(labels).to eq ["a", "b", "c", "d", "e"]
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,38 +1,26 @@
1
1
  require "spec_helper"
2
2
 
3
- class BaseRenderer < Redcarpet::Render::HTML
4
- include Graphdown::Renderable
5
- end
6
-
7
3
  describe Graphdown::Renderable do
8
- let(:markdown) { Redcarpet::Markdown.new(BaseRenderer, fenced_code_blocks: true) }
9
- let(:fixtures_path) { Pathname.new("spec/fixtures") }
10
- let(:dot_path) { Pathname.pwd.join("sample.dot") }
11
- let(:graph_path) { Pathname.pwd.join("sample.png") }
12
-
13
- context "when the language of block code is dot" do
14
- let(:content) { fixtures_path.join("sample.md").read }
15
- after do
16
- graph_path.delete if graph_path.exist?
17
- end
4
+ let(:markdown) do
5
+ renderer = Class.new(Redcarpet::Render::HTML)
6
+ renderer.send(:include, Graphdown::Renderable)
7
+ Redcarpet::Markdown.new(renderer)
8
+ end
9
+ let(:fixtures_path) { Pathname.new("./spec/fixtures") }
18
10
 
19
- it "generates img tag" do
11
+ context "when graph text is passed" do
12
+ it "generates svg tag" do
13
+ content = fixtures_path.join("sample.md").read
20
14
  html = markdown.render(content)
21
- expect(html).to match /<img src="#{graph_path.to_s}"\/>/
22
- end
23
-
24
- it "generates graph image file" do
25
- markdown.render(content)
26
- expect(graph_path).to be_exist
15
+ expect(html).to match /<svg.+>.+<\/svg>/m
27
16
  end
28
17
  end
29
18
 
30
- context "when the language of block code isn't dot" do
31
- let(:content) { fixtures_path.join("sample_without_graph.md").read }
32
-
33
- it "generates code tag" do
19
+ context "when normal text is passed" do
20
+ it "generates p tag" do
21
+ content = fixtures_path.join("sample_without_graph.md").read
34
22
  html = markdown.render(content)
35
- expect(html).to match /<code>.+<\/code>/m
23
+ expect(html).to match /<p>.+<\/p>/
36
24
  end
37
25
  end
38
26
  end
data/spec/spec_helper.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require File.expand_path("../lib/graphdown", File.dirname(__FILE__))
2
2
  require "pathname"
3
+ require "redcarpet"
3
4
 
4
5
  RSpec.configure do |config|
5
6
  config.treat_symbols_as_metadata_keys_with_true_values = true
metadata CHANGED
@@ -1,92 +1,136 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphdown
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Naoto Kaneko
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-01-21 00:00:00.000000000 Z
11
+ date: 2014-02-16 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: svgen
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: thor
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.18.1
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.18.1
13
41
  - !ruby/object:Gem::Dependency
14
42
  name: redcarpet
15
43
  requirement: !ruby/object:Gem::Requirement
16
44
  requirements:
17
- - - ~>
45
+ - - "~>"
18
46
  - !ruby/object:Gem::Version
19
47
  version: 3.0.0
20
48
  type: :runtime
21
49
  prerelease: false
22
50
  version_requirements: !ruby/object:Gem::Requirement
23
51
  requirements:
24
- - - ~>
52
+ - - "~>"
25
53
  - !ruby/object:Gem::Version
26
54
  version: 3.0.0
27
55
  - !ruby/object:Gem::Dependency
28
56
  name: bundler
29
57
  requirement: !ruby/object:Gem::Requirement
30
58
  requirements:
31
- - - ~>
59
+ - - "~>"
32
60
  - !ruby/object:Gem::Version
33
61
  version: '1.5'
34
62
  type: :development
35
63
  prerelease: false
36
64
  version_requirements: !ruby/object:Gem::Requirement
37
65
  requirements:
38
- - - ~>
66
+ - - "~>"
39
67
  - !ruby/object:Gem::Version
40
68
  version: '1.5'
41
69
  - !ruby/object:Gem::Dependency
42
70
  name: rake
43
71
  requirement: !ruby/object:Gem::Requirement
44
72
  requirements:
45
- - - '>='
73
+ - - ">="
46
74
  - !ruby/object:Gem::Version
47
75
  version: '0'
48
76
  type: :development
49
77
  prerelease: false
50
78
  version_requirements: !ruby/object:Gem::Requirement
51
79
  requirements:
52
- - - '>='
80
+ - - ">="
53
81
  - !ruby/object:Gem::Version
54
82
  version: '0'
55
83
  - !ruby/object:Gem::Dependency
56
84
  name: rspec
57
85
  requirement: !ruby/object:Gem::Requirement
58
86
  requirements:
59
- - - ~>
87
+ - - "~>"
60
88
  - !ruby/object:Gem::Version
61
89
  version: 2.14.1
62
90
  type: :development
63
91
  prerelease: false
64
92
  version_requirements: !ruby/object:Gem::Requirement
65
93
  requirements:
66
- - - ~>
94
+ - - "~>"
67
95
  - !ruby/object:Gem::Version
68
96
  version: 2.14.1
69
97
  description:
70
98
  email:
71
99
  - naoty.k@gmail.com
72
- executables: []
100
+ executables:
101
+ - graphdown
73
102
  extensions: []
74
103
  extra_rdoc_files: []
75
104
  files:
76
- - .gitignore
105
+ - ".gitignore"
106
+ - ".travis.yml"
77
107
  - Gemfile
78
108
  - LICENSE.txt
79
109
  - README.md
80
110
  - Rakefile
81
- - examples/parser.rb
82
- - examples/sample.html
83
- - examples/sample.md
84
- - examples/sample.png
111
+ - bin/graphdown
112
+ - examples/path_to_graphdown.html
113
+ - examples/path_to_graphdown.md
114
+ - examples/path_to_graphdown.png
115
+ - examples/servers_arrangement.html
116
+ - examples/servers_arrangement.md
117
+ - examples/servers_arrangement.png
85
118
  - graphdown.gemspec
86
119
  - lib/graphdown.rb
120
+ - lib/graphdown/cli.rb
121
+ - lib/graphdown/edge.rb
122
+ - lib/graphdown/graph.rb
123
+ - lib/graphdown/node.rb
124
+ - lib/graphdown/parser.rb
125
+ - lib/graphdown/point.rb
87
126
  - lib/graphdown/renderable.rb
127
+ - lib/graphdown/version.rb
88
128
  - spec/fixtures/sample.md
129
+ - spec/fixtures/sample.svg
89
130
  - spec/fixtures/sample_without_graph.md
131
+ - spec/graphdown/graph_spec.rb
132
+ - spec/graphdown/node_spec.rb
133
+ - spec/graphdown/parser_spec.rb
90
134
  - spec/graphdown/renderable_spec.rb
91
135
  - spec/spec_helper.rb
92
136
  homepage: https://github.com/naoty/graphdown
@@ -99,22 +143,26 @@ require_paths:
99
143
  - lib
100
144
  required_ruby_version: !ruby/object:Gem::Requirement
101
145
  requirements:
102
- - - '>='
146
+ - - ">="
103
147
  - !ruby/object:Gem::Version
104
148
  version: '0'
105
149
  required_rubygems_version: !ruby/object:Gem::Requirement
106
150
  requirements:
107
- - - '>='
151
+ - - ">="
108
152
  - !ruby/object:Gem::Version
109
153
  version: '0'
110
154
  requirements: []
111
155
  rubyforge_project:
112
- rubygems_version: 2.0.14
156
+ rubygems_version: 2.2.0
113
157
  signing_key:
114
158
  specification_version: 4
115
159
  summary: Markdown extension for embedding graphs.
116
160
  test_files:
117
161
  - spec/fixtures/sample.md
162
+ - spec/fixtures/sample.svg
118
163
  - spec/fixtures/sample_without_graph.md
164
+ - spec/graphdown/graph_spec.rb
165
+ - spec/graphdown/node_spec.rb
166
+ - spec/graphdown/parser_spec.rb
119
167
  - spec/graphdown/renderable_spec.rb
120
168
  - spec/spec_helper.rb
data/examples/parser.rb DELETED
@@ -1,13 +0,0 @@
1
- require "redcarpet"
2
- require "graphdown"
3
-
4
- class BaseRenderer < Redcarpet::Render::HTML
5
- include Graphdown::Renderable
6
- end
7
-
8
- markdown = Redcarpet::Markdown.new(BaseRenderer, fenced_code_blocks: true)
9
- File.open("sample.md", "rb") do |file|
10
- content = file.read
11
- html = markdown.render(content)
12
- File.open("sample.html", "wb") { |file| file.write(html) }
13
- end
data/examples/sample.html DELETED
@@ -1,8 +0,0 @@
1
- <h1>Views transition</h1>
2
- <img src="/Users/naoty/workspace/ruby/graphdown/examples/sample.png"/>
3
- <ul>
4
- <li>Users visit show.html from index.html.</li>
5
- <li>Users visit index.html from show.html.</li>
6
- <li>Users visit new.html from index.html.</li>
7
- <li>Users are redirected to index.html from new.html.</li>
8
- </ul>
data/examples/sample.md DELETED
@@ -1,18 +0,0 @@
1
- # Views transition
2
-
3
- ```dot
4
- digraph sample {
5
- A [label = "index.html"];
6
- B [label = "show.html"];
7
- C [label = "new.html"];
8
-
9
- A -> B [dir = both];
10
- A -> C;
11
- C -> A [label = "redirect"];
12
- }
13
- ```
14
-
15
- - Users visit show.html from index.html.
16
- - Users visit index.html from show.html.
17
- - Users visit new.html from index.html.
18
- - Users are redirected to index.html from new.html.
data/examples/sample.png DELETED
Binary file