SAVAGE 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,13 @@
1
+ Copyright 2013 Mason Simon
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
@@ -0,0 +1,69 @@
1
+ # SAVAGE
2
+
3
+ ## Intro
4
+
5
+ Savage is a graphics language layered on top of SVG, in the spirit of Sass and Haml.
6
+
7
+ ## Usage
8
+
9
+ 1. clone repo
10
+ 1. bundle
11
+ 1. bundle exec ruby compiler.rb < test.svge > test.svg
12
+ 1. View test.svg in a web browser or other SVG renderer.
13
+
14
+ ## Example
15
+
16
+ // Set dimensions of your graphic (width x height).
17
+ 400 x 300
18
+
19
+ // Draw a circle of radius 100 at (200,150).
20
+ (100) @ 200,150
21
+
22
+ // Draw an ellipse with x-radius 50 and y-radius 40 at 10,10.
23
+ (50,40) @ 10,10
24
+
25
+ // Draw a square of side length 10.
26
+ [10] @ 100,200
27
+
28
+ // Draw a rectangle of width 50, height 30.
29
+ [50,30] @ 300,0
30
+
31
+ // Draw a line from (20,30) to (40,50), then to (50,60).
32
+ -
33
+ @ 20,30
34
+ @ 40,50
35
+ @ 50,60
36
+
37
+ // Draw an arrow.
38
+ ~
39
+ M 100,225
40
+ L 100,115
41
+ L 130,115
42
+ L 70,15
43
+ L 10,115
44
+ L 40,115
45
+ L 40,225
46
+ z
47
+
48
+ // Style using Sass.
49
+ :sass
50
+ .dot
51
+ fill: red
52
+
53
+ #point
54
+ fill: blue
55
+
56
+ [15].dot @ 300,250
57
+
58
+ [15].dot @ 350,270
59
+
60
+ (15)#point @ 360, 230
61
+
62
+ ## License
63
+
64
+ Apache 2.0. See LICENSE.txt.
65
+
66
+ ## Credits
67
+
68
+ Marc-André Cournoyer for writing [http://createyourproglang.com/](http://createyourproglang.com/), which got this project off the ground. The authors of [Haml](http://haml.info/), [Sass](http://sass-lang.com/), and [Coffeescript](http://coffeescript.org/), for demonstrating that we need not be content with the languages W3C gives us.
69
+
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
4
+ require 'lexer'
5
+ require 'parser'
6
+
7
+ tokens = Lexer.new.tokenize(ARGF.read)
8
+ parsed = Parser.new(tokens)
9
+
10
+ puts '<?xml version="1.0" standalone="yes"?>'
11
+ puts parsed.translate
12
+
@@ -0,0 +1,100 @@
1
+ class Lexer
2
+ def tokenize(code)
3
+ code.chomp!
4
+
5
+ tokens = []
6
+
7
+ # TODO: Switch to a state machine framework (state_machine, Ragel);
8
+ # this is getting gross.
9
+
10
+ # Advance 1 char at a time until finding something parsable.
11
+ i = 0
12
+
13
+ indented = false
14
+
15
+ while i < code.size
16
+ chunk = code[i..-1]
17
+
18
+ if comment = chunk.match(/\A\/\/.*?\n/)
19
+ i += comment.to_s.size - 1
20
+ elsif dimension = chunk.match(/\A(\d+)\sx\s(\d+)/)
21
+ w, h = dimension.captures.map(&:to_i)
22
+ tokens << [:DIMENSION, [w, h]]
23
+ i += dimension.to_s.size
24
+ elsif sass = chunk.match(/\A\:sass\n((?:(?:[ ]+.*?\n)|\n)+)/)
25
+ src = sass[1]
26
+ indented = true
27
+ tokens << [:SASS, src]
28
+ i += sass.to_s.size - 1
29
+ elsif circle = chunk.match(/\A\((\d+)\)/)
30
+ r = circle[1].to_i
31
+ tokens << [:CIRCLE, r]
32
+ i += circle.to_s.size
33
+ elsif ellipse = chunk.match(/\A\((\d+),\s*(\d+)\)/)
34
+ rx, ry = ellipse.captures.map(&:to_i)
35
+ tokens << [:ELLIPSE, [rx, ry]]
36
+ i += ellipse.to_s.size
37
+ elsif round_rect = chunk.match(/\A\[(\d+),\s*(\d+),\s*(\d+),\s*(\d+)\)/)
38
+ w, h, rx, ry = round_rect.captures.map(&:to_i)
39
+ tokens << [:ROUND_RECT, [w, h, rx, ry]]
40
+ i += round_rect.to_s.size
41
+ elsif rect = chunk.match(/\A\[(\d+),\s*(\d+)\]/)
42
+ w, h = rect.captures.map(&:to_i)
43
+ tokens << [:RECT, [w, h]]
44
+ i += rect.to_s.size
45
+ elsif square = chunk.match(/\A\[(\d+)\]/)
46
+ s = square[1].to_i
47
+ tokens << [:RECT, [s, s]]
48
+ i += square.to_s.size
49
+ elsif text = chunk.match(/\A\"([^\"]+)\"/)
50
+ t = text[1]
51
+ tokens << [:TEXT, t]
52
+ i += text.to_s.size
53
+ elsif line = chunk.match(/\A\-/)
54
+ tokens << [:LINE]
55
+ i += line.to_s.size
56
+ elsif path = chunk.match(/\A\~/)
57
+ tokens << [:PATH]
58
+ i += path.to_s.size
59
+ elsif pathdef = chunk.match(/\A([MmZzLlHhVvCcSsQqTtAa])\s?((?:[\d\.\-,](?: )?)+)?/)
60
+ command, params = pathdef.captures
61
+ tokens << [:PATHDEF, [command, params]]
62
+ i += pathdef.to_s.size
63
+ elsif id = chunk.match(/\A#([a-zA-Z0-9_\-]+)/)
64
+ name = id[1]
65
+ tokens << [:ID, name]
66
+ i += id.to_s.size
67
+ elsif classname = chunk.match(/\A\.([a-zA-Z0-9_\-]+)/)
68
+ name = classname[1]
69
+ tokens << [:CLASS, name]
70
+ i += classname.to_s.size
71
+ elsif coords = chunk.match(/\A@\s*(\d+)\s*,\s*(\d+)/)
72
+ x, y = coords.captures
73
+ tokens << [:LOCATION, [x.to_i, y.to_i]]
74
+ i += coords.to_s.size
75
+ elsif chunk.match(/\A\n\n/)
76
+ tokens << [:NEWLINE]
77
+ i += 1
78
+ elsif indent = chunk.match(/\A\n\s+/)
79
+ indented = true
80
+ tokens << [:INDENT]
81
+ i += indent.to_s.size
82
+ elsif indented && chunk.match(/\A\n[^\s]/)
83
+ indented = false
84
+ tokens << [:DEDENT]
85
+ i += 1
86
+ elsif indented && (attribute = chunk.match(/\A(\w+):\s*([^\s]+?)(?:\n|\Z)/))
87
+ key, value = attribute.captures
88
+ tokens << [:ATTRIBUTE, [key, value]]
89
+ i += attribute.to_s.size - 1 # Don't advance beyond newline.
90
+ else
91
+ i += 1
92
+ end
93
+ end
94
+ tokens << [:EOF]
95
+
96
+ tokens
97
+ end
98
+ end
99
+
100
+
@@ -0,0 +1,82 @@
1
+ require_relative 'parsers/svg'
2
+ require_relative 'parsers/sass'
3
+ require_relative 'parsers/circle'
4
+ require_relative 'parsers/ellipse'
5
+ require_relative 'parsers/round_rect'
6
+ require_relative 'parsers/rect'
7
+ require_relative 'parsers/text'
8
+ require_relative 'parsers/line'
9
+ require_relative 'parsers/path'
10
+
11
+ class Parser
12
+ attr_reader :els
13
+
14
+ def initialize(tokens)
15
+ @els = Svg.new
16
+ curr = @els
17
+
18
+ # TODO: Switch to a state machine framework (state_machine, Ragel);
19
+ # this is getting gross.
20
+ tokens.each do |token|
21
+ if curr == @els
22
+ if token.first == :DIMENSION
23
+ el = Svg.new
24
+ el << token
25
+ @els = el
26
+ curr = el
27
+ elsif token.first == :SASS
28
+ el = SassEl.new
29
+ el << token
30
+ curr << el
31
+ curr = el
32
+ elsif token.first == :CIRCLE
33
+ el = Circle.new
34
+ el << token
35
+ curr << el
36
+ curr = el
37
+ elsif token.first == :ELLIPSE
38
+ el = Ellipse.new
39
+ el << token
40
+ curr << el
41
+ curr = el
42
+ elsif token.first == :ROUND_RECT
43
+ el = RoundRect.new
44
+ el << token
45
+ curr << el
46
+ curr = el
47
+ elsif token.first == :RECT
48
+ el = Rect.new
49
+ el << token
50
+ curr << el
51
+ curr = el
52
+ elsif token.first == :TEXT
53
+ el = Text.new
54
+ el << token
55
+ curr << el
56
+ curr = el
57
+ elsif token.first == :LINE
58
+ el = Line.new
59
+ el << token
60
+ curr << el
61
+ curr = el
62
+ elsif token.first == :PATH
63
+ el = Path.new
64
+ el << token
65
+ curr << el
66
+ curr = el
67
+ end
68
+ else
69
+ if [:EOF, :DEDENT, :NEWLINE].include? token.first
70
+ curr = @els
71
+ else
72
+ curr << token
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ def translate
79
+ @els.translate
80
+ end
81
+ end
82
+
@@ -0,0 +1,28 @@
1
+ require_relative 'element'
2
+
3
+ class Circle < Element
4
+ def <<(token)
5
+ if @tokens.empty?
6
+ raise "Invalid starting token: #{token}" unless token.first == :CIRCLE
7
+ radius = token.last
8
+ @attributes['r'] = radius
9
+ end
10
+
11
+ if token.first == :LOCATION
12
+ x, y = token.last
13
+ @attributes['cx'] = x
14
+ @attributes['cy'] = y
15
+ elsif token.first == :ATTRIBUTE
16
+ k, v = token.last
17
+ @attributes[k] = v
18
+ end
19
+
20
+ super(token)
21
+ end
22
+
23
+ def translate
24
+ attrs = @attributes.map { |k,v| "#{k}='#{v}'" }.join(' ')
25
+ "<circle #{attrs} />"
26
+ end
27
+ end
28
+
@@ -0,0 +1,20 @@
1
+ class Element
2
+ def initialize
3
+ @tokens = []
4
+ @attributes = {}
5
+ @children = []
6
+ end
7
+
8
+ def <<(token)
9
+ if token.is_a? Array
10
+ if token.first == :ID
11
+ @attributes['id'] = token.last
12
+ elsif token.first == :CLASS
13
+ @attributes['class'] = token.last
14
+ end
15
+ end
16
+
17
+ @tokens << token
18
+ end
19
+ end
20
+
@@ -0,0 +1,29 @@
1
+ require_relative 'element'
2
+
3
+ class Ellipse < Element
4
+ def <<(token)
5
+ if @tokens.empty?
6
+ raise "Invalid starting token: #{token}" unless token.first == :ELLIPSE
7
+ rx, ry = token.last
8
+ @attributes['rx'] = rx
9
+ @attributes['ry'] = ry
10
+ end
11
+
12
+ if token.first == :LOCATION
13
+ x, y = token.last
14
+ @attributes['cx'] = x
15
+ @attributes['cy'] = y
16
+ elsif token.first == :ATTRIBUTE
17
+ k, v = token.last
18
+ @attributes[k] = v
19
+ end
20
+
21
+ super(token)
22
+ end
23
+
24
+ def translate
25
+ attrs = @attributes.map { |k,v| "#{k}='#{v}'" }.join(' ')
26
+ "<ellipse #{attrs} />"
27
+ end
28
+ end
29
+
@@ -0,0 +1,31 @@
1
+ require_relative 'element'
2
+
3
+ class Line < Element
4
+ def initialize
5
+ super
6
+ @points = []
7
+ end
8
+
9
+ def <<(token)
10
+ if @tokens.empty?
11
+ raise "Invalid starting token: #{token}" unless token.first == :LINE
12
+ end
13
+
14
+ if token.first == :LOCATION
15
+ x, y = token.last
16
+ @points << [x, y]
17
+ elsif token.first == :ATTRIBUTE
18
+ k, v = token.last
19
+ @attributes[k] = v
20
+ end
21
+
22
+ super(token)
23
+ end
24
+
25
+ def translate
26
+ attrs = @attributes.map { |k,v| "#{k}='#{v}'" }.join(' ')
27
+ points = @points.map { |coords| coords.join(',') }.join(' ')
28
+ "<polyline points='#{points}' #{attrs} />"
29
+ end
30
+ end
31
+
@@ -0,0 +1,34 @@
1
+ require_relative 'element'
2
+
3
+ class Path < Element
4
+ def initialize
5
+ super
6
+ @pathdefs = []
7
+ end
8
+
9
+ def <<(token)
10
+ if @tokens.empty?
11
+ raise "Invalid starting token: #{token}" unless token.first == :PATH
12
+ end
13
+
14
+ if token.first == :LOCATION
15
+ x, y = token.last
16
+ @points << [x, y]
17
+ elsif token.first == :ATTRIBUTE
18
+ k, v = token.last
19
+ @attributes[k] = v
20
+ elsif token.first == :PATHDEF
21
+ command, params = token.last
22
+ @pathdefs << [command, params]
23
+ end
24
+
25
+ super(token)
26
+ end
27
+
28
+ def translate
29
+ attrs = @attributes.map { |k,v| "#{k}='#{v}'" }.join(' ')
30
+ definition = @pathdefs.map { |command, params| "#{command} #{params}" }.join(' ')
31
+ "<path d='#{definition}' #{attrs} />"
32
+ end
33
+ end
34
+
@@ -0,0 +1,29 @@
1
+ require_relative 'element'
2
+
3
+ class Rect < Element
4
+ def <<(token)
5
+ if @tokens.empty?
6
+ raise "Invalid starting token: #{token}" unless token.first == :RECT
7
+ w, h = token.last
8
+ @attributes['width'] = w
9
+ @attributes['height'] = h
10
+ end
11
+
12
+ if token.first == :LOCATION
13
+ x, y = token.last
14
+ @attributes['x'] = x
15
+ @attributes['y'] = y
16
+ elsif token.first == :ATTRIBUTE
17
+ k, v = token.last
18
+ @attributes[k] = v
19
+ end
20
+
21
+ super(token)
22
+ end
23
+
24
+ def translate
25
+ attrs = @attributes.map { |k,v| "#{k}='#{v}'" }.join(' ')
26
+ "<rect #{attrs} />"
27
+ end
28
+ end
29
+
@@ -0,0 +1,31 @@
1
+ require_relative 'element'
2
+
3
+ class RoundRect < Element
4
+ def <<(token)
5
+ if @tokens.empty?
6
+ raise "Invalid starting token: #{token}" unless token.first == :ROUND_RECT
7
+ w, h, rx, ry = token.last
8
+ @attributes['width'] = w
9
+ @attributes['height'] = h
10
+ @attributes['rx'] = rx
11
+ @attributes['ry'] = ry
12
+ end
13
+
14
+ if token.first == :LOCATION
15
+ x, y = token.last
16
+ @attributes['x'] = x
17
+ @attributes['y'] = y
18
+ elsif token.first == :ATTRIBUTE
19
+ k, v = token.last
20
+ @attributes[k] = v
21
+ end
22
+
23
+ super(token)
24
+ end
25
+
26
+ def translate
27
+ attrs = @attributes.map { |k,v| "#{k}='#{v}'" }.join(' ')
28
+ "<rect #{attrs} />"
29
+ end
30
+ end
31
+
@@ -0,0 +1,27 @@
1
+ require_relative 'element'
2
+ require 'sass'
3
+
4
+ class SassEl < Element
5
+ def <<(token)
6
+ if @tokens.empty?
7
+ raise "Invalid starting token: #{token}" unless token.first == :SASS
8
+
9
+ # Dedent the text.
10
+ lines = token.last.split("\n")
11
+ indent = lines.first.match(/\A(\s+)/)[1].size
12
+ @sass = lines.map { |l| l[indent..-1] }.join("\n")
13
+ end
14
+
15
+ super(token)
16
+ end
17
+
18
+ def translate
19
+ css = Sass::Engine.new(@sass).render
20
+ """<style type='text/css' media='screen'>
21
+ <![CDATA[
22
+ #{css}
23
+ ]]>
24
+ </style>"""
25
+ end
26
+ end
27
+
@@ -0,0 +1,41 @@
1
+ require_relative 'element'
2
+
3
+ class Svg < Element
4
+ def initialize
5
+ super
6
+ @attributes = {
7
+ 'width' => 100,
8
+ 'height' => 100,
9
+ 'version' => '1.1',
10
+ 'xmlns' => 'http://www.w3.org/2000/svg',
11
+ }
12
+ end
13
+
14
+ def <<(token)
15
+ if @tokens.empty?
16
+ raise "Invalid starting token: #{token}" unless token.first == :DIMENSION
17
+ w, h = token.last
18
+ @attributes['width'] = w
19
+ @attributes['height'] = h
20
+ end
21
+
22
+ if token.is_a? Array
23
+ if token.first == :ATTRIBUTE
24
+ k, v = token.last
25
+ @attributes[k] = v
26
+ end
27
+ elsif token.class < Element
28
+ @children << token
29
+ end
30
+
31
+ super(token)
32
+ end
33
+
34
+ def translate
35
+ attrs = @attributes.map { |k,v| "#{k}='#{v}'" }.join(' ')
36
+ """<svg #{attrs}>
37
+ #{@children.map { |el| el.translate }.join("\n")}
38
+ </svg>"""
39
+ end
40
+ end
41
+
@@ -0,0 +1,27 @@
1
+ require_relative 'element'
2
+
3
+ class Text < Element
4
+ def <<(token)
5
+ if @tokens.empty?
6
+ raise "Invalid starting token: #{token}" unless token.first == :TEXT
7
+ @text = token.last
8
+ end
9
+
10
+ if token.first == :LOCATION
11
+ x, y = token.last
12
+ @attributes['x'] = x
13
+ @attributes['y'] = y
14
+ elsif token.first == :ATTRIBUTE
15
+ k, v = token.last
16
+ @attributes[k] = v
17
+ end
18
+
19
+ super(token)
20
+ end
21
+
22
+ def translate
23
+ attrs = @attributes.map { |k,v| "#{k}='#{v}'" }.join(' ')
24
+ "<text #{attrs}>#{@text}</text>"
25
+ end
26
+ end
27
+
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: SAVAGE
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Mason Simon
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-06-17 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: sass
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: Savage is a graphics language layered on top of SVG, in the spirit of
31
+ Sass and Haml.
32
+ email: masonsimon@gmail.com
33
+ executables:
34
+ - savage
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - lib/lexer.rb
39
+ - lib/parser.rb
40
+ - lib/parsers/circle.rb
41
+ - lib/parsers/element.rb
42
+ - lib/parsers/ellipse.rb
43
+ - lib/parsers/line.rb
44
+ - lib/parsers/path.rb
45
+ - lib/parsers/rect.rb
46
+ - lib/parsers/round_rect.rb
47
+ - lib/parsers/sass.rb
48
+ - lib/parsers/svg.rb
49
+ - lib/parsers/text.rb
50
+ - bin/savage
51
+ - LICENSE.txt
52
+ - README.md
53
+ homepage: https://bitbucket.org/masonicboom/savage
54
+ licenses: []
55
+ post_install_message:
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ none: false
61
+ requirements:
62
+ - - ! '>='
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ! '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ requirements: []
72
+ rubyforge_project:
73
+ rubygems_version: 1.8.23
74
+ signing_key:
75
+ specification_version: 3
76
+ summary: Text-editor friendly graphics language that compiles to SVG.
77
+ test_files: []