graphdown 0.0.1 → 0.1.0

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.
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