SAVAGE 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.
@@ -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: []