swift-playground 0.0.1

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.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.ruby-version +1 -0
  4. data/Gemfile +19 -0
  5. data/LICENSE +22 -0
  6. data/README.md +209 -0
  7. data/Rakefile +16 -0
  8. data/bin/swift-playground +5 -0
  9. data/lib/swift/playground.rb +193 -0
  10. data/lib/swift/playground/asset.rb +52 -0
  11. data/lib/swift/playground/assets/javascript.rb +9 -0
  12. data/lib/swift/playground/assets/stylesheet.rb +36 -0
  13. data/lib/swift/playground/cli.rb +29 -0
  14. data/lib/swift/playground/cli/commands/generate.rb +95 -0
  15. data/lib/swift/playground/cli/commands/new.rb +49 -0
  16. data/lib/swift/playground/cli/definition.rb +32 -0
  17. data/lib/swift/playground/cli/global/error_handling.rb +34 -0
  18. data/lib/swift/playground/cli/shared_attributes.rb +19 -0
  19. data/lib/swift/playground/cli/ui.rb +120 -0
  20. data/lib/swift/playground/debug.rb +4 -0
  21. data/lib/swift/playground/generator.rb +32 -0
  22. data/lib/swift/playground/metadata.rb +12 -0
  23. data/lib/swift/playground/section.rb +117 -0
  24. data/lib/swift/playground/sections/code_section.rb +19 -0
  25. data/lib/swift/playground/sections/documentation_section.rb +63 -0
  26. data/lib/swift/playground/template/Documentation/defaults.css.scss +96 -0
  27. data/lib/swift/playground/template/Documentation/section.html.erb +23 -0
  28. data/lib/swift/playground/template/contents.xcplayground.erb +8 -0
  29. data/lib/swift/playground/util.rb +3 -0
  30. data/lib/swift/playground/util/path_or_content.rb +31 -0
  31. data/lib/swift/playground/util/pipeline.rb +53 -0
  32. data/lib/swift/playground/util/pipeline/section_filter.rb +55 -0
  33. data/lib/swift/playground/util/pipeline/unicode_emoji_filter.rb +27 -0
  34. data/lib/swift/playground/util/syntax_highlighting.rb +24 -0
  35. data/swift-playground.gemspec +29 -0
  36. metadata +207 -0
@@ -0,0 +1,4 @@
1
+ begin
2
+ require 'pry'
3
+ rescue LoadError => e
4
+ end
@@ -0,0 +1,32 @@
1
+ require_relative 'util'
2
+
3
+ module Swift
4
+ class Playground
5
+ class Generator
6
+ class << self
7
+ include Util::PathOrContent
8
+
9
+ def generate(markdown, options={})
10
+ markdown_file = path_or_content_as_io(markdown)
11
+
12
+ playground = Playground.new
13
+
14
+ pipeline = Util::Pipeline.new(Util::Pipeline::MarkdownFilterChain)
15
+ converted_markdown = pipeline.call(markdown_file.read)[:output]
16
+ converted_markdown.xpath('./section').each do |section|
17
+ case section[:role]
18
+ when 'documentation'
19
+ html = section.inner_html
20
+ playground.sections << DocumentationSection.new(html)
21
+ when 'code'
22
+ code = section.xpath('./pre/code').inner_text
23
+ playground.sections << CodeSection.new(code)
24
+ end
25
+ end
26
+
27
+ playground
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,12 @@
1
+ module Swift
2
+ class Playground
3
+ NAME = 'swift-playground'
4
+ SUMMARY = 'Create Xcode Swift Playgrounds, including generating from ' \
5
+ 'Markdown files.'
6
+ DESCRIPTION = 'A Ruby API and CLI tool for manipulating Xcode Swift ' \
7
+ 'Playgrounds. Supports generation from markdown files ' \
8
+ 'with the intent to aide in the production of polished ' \
9
+ 'playground documents.'
10
+ VERSION = '0.0.1'
11
+ end
12
+ end
@@ -0,0 +1,117 @@
1
+ require 'pathname'
2
+
3
+ module Swift
4
+ class Playground
5
+ sections_path = Pathname.new('swift/playground/sections')
6
+ autoload :DocumentationSection, sections_path.join('documentation_section')
7
+ autoload :CodeSection, sections_path.join('code_section')
8
+
9
+ class Section
10
+ include Util::PathOrContent
11
+
12
+ class TemplateContext
13
+ attr_accessor :content, :number
14
+
15
+ extend Forwardable
16
+ def_delegators :@playground, :stylesheets, :javascripts
17
+
18
+ def self.context(*args)
19
+ new(*args).instance_eval { binding }
20
+ end
21
+
22
+ def context
23
+ binding
24
+ end
25
+
26
+ def initialize(content, number, playground)
27
+ @content = content
28
+ @number = number
29
+ @playground = playground
30
+ end
31
+ end
32
+
33
+ attr_reader :content
34
+
35
+ class << self
36
+ protected
37
+
38
+ def template
39
+ unless defined? @template
40
+ template_root = Pathname.new('../template').expand_path(__FILE__)
41
+ template_path = template_root.join(@directory, "section.#{@extension}.erb")
42
+
43
+ if template_path.exist?
44
+ template_contents = template_path.read
45
+ else
46
+ template_contents = '<%= content %>'
47
+ end
48
+ @template = ERB.new(template_contents)
49
+ end
50
+
51
+ @template
52
+ end
53
+
54
+ def extension(extension = nil)
55
+ @extension = extension unless extension.nil?
56
+ @extension
57
+ end
58
+
59
+ def directory(path = nil)
60
+ @directory = Pathname.new(path || '') unless path.nil?
61
+ @directory
62
+ end
63
+
64
+ def xcplayground(options = nil)
65
+ @xcplayground_options = options unless options.nil?
66
+ @xcplayground_options
67
+ end
68
+ end
69
+
70
+ def initialize(content)
71
+ @content = path_or_content_as_io(content).read
72
+ @content.freeze
73
+ end
74
+
75
+ def filename(number)
76
+ "section-#{number}.#{extension}"
77
+ end
78
+
79
+ def path(number)
80
+ directory.join filename(number)
81
+ end
82
+
83
+ def xcplayground_node(number)
84
+ options = xcplayground_options
85
+
86
+ node = Nokogiri::XML.fragment("<#{options[:node]}>").children.first
87
+ node[options[:path_attribute]] = path(number).relative_path_from(directory)
88
+ node
89
+ end
90
+
91
+ def render(number, playground, custom_content = nil)
92
+ context = TemplateContext.context custom_content || content,
93
+ number,
94
+ playground
95
+ template.result(context)
96
+ end
97
+
98
+ protected
99
+
100
+ def template
101
+ self.class.send(:template)
102
+ end
103
+
104
+ def extension
105
+ self.class.send(:extension)
106
+ end
107
+
108
+ def directory
109
+ self.class.send(:directory)
110
+ end
111
+
112
+ def xcplayground_options
113
+ self.class.send(:xcplayground)
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,19 @@
1
+ module Swift
2
+ class Playground
3
+ class CodeSection < Section
4
+ extension 'swift'
5
+ directory false
6
+
7
+ xcplayground node: 'code',
8
+ path_attribute: 'source-file-name'
9
+
10
+ attr_accessor :style
11
+
12
+ def xcplayground_node(number)
13
+ node = super(number)
14
+ node['style'] = 'setup' if style == 'setup'
15
+ node
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,63 @@
1
+ module Swift
2
+ class Playground
3
+ class DocumentationSection < Section
4
+ extension 'html'
5
+ directory 'Documentation'
6
+
7
+ xcplayground node: 'documentation',
8
+ path_attribute: 'relative-path'
9
+
10
+ attr_reader :assets
11
+
12
+ def initialize(content)
13
+ super(content)
14
+
15
+ if @content =~ /(<html|<head|<body)[\s>]/
16
+ raise 'Please provide an HTML fragment only. ' +
17
+ 'Do not include an <html>, <head> or <body> tag.'
18
+ end
19
+
20
+ extract_assets
21
+ end
22
+
23
+ def render(number, playground)
24
+ pipeline = Util::Pipeline.new
25
+ if playground.convert_emoji?
26
+ pipeline.filters << Util::Pipeline::EmojiFilter
27
+ end
28
+
29
+ if playground.syntax_highlighting
30
+ if Util::SyntaxHighlighting.available?
31
+ pipeline.filters << Util::Pipeline::SyntaxHighlightFilter
32
+ else
33
+ $stderr.puts "WARNING: Unable to highlight syntax for section " +
34
+ "#{number}, please make sure that github-linguist " +
35
+ "and pygments.rb gems are installed."
36
+ end
37
+ end
38
+
39
+ if pipeline.has_filters?
40
+ processed = pipeline.call(content)
41
+ super(number, playground, processed[:output].inner_html)
42
+ else
43
+ super(number, playground)
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def extract_assets
50
+ @assets = []
51
+
52
+ document = Nokogiri::HTML(@content)
53
+ document.search('//img[@src]').each do |img|
54
+ image_path = Pathname.new(img['src'])
55
+
56
+ if image_path.relative?
57
+ @assets << Asset.new(img['src'])
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,96 @@
1
+ $playground_font_family: "Helvetica Neue", Helvetica, sans-serif !default;
2
+ $playground_font_size: 1.1rem !default;
3
+ $playground_background_color: #fff !default;
4
+ $playground_text_inset: 6px !default;
5
+
6
+ $playground_section_separator_height: 1px !default;
7
+ $playground_section_separator_color: #e7e7e7 !default;
8
+
9
+ $playground_gutter_width: 28px !default;
10
+ $playground_gutter_color: #fff !default;
11
+ $playground_gutter_right_margin_inset: 8px !default;
12
+ $playground_gutter_right_margin_line_width: 1px !default;
13
+ $playground_gutter_right_margin_line_style: solid !default;
14
+ $playground_gutter_right_margin_line_color: $playground_section_separator_color !default;
15
+
16
+ // This font adjustment is so that 1rem of Menlo will appear the same size in
17
+ // the HTML sections as it does in the editable swift code sections. For some
18
+ // reason Xcode (6.1.1 - 6A2008a) adds 3 px to the font-size in HTML sections
19
+ // when compared against the font-size used in those code sections. The 'calc'
20
+ // using this adjustment variable compensates for this:
21
+ $playground_font_adjustment: 3px !default;
22
+ // This separator buffer is needed to avoid the bottom border of a section being
23
+ // reduced by half a pixel sometimes. Instead a transparent 0.5-1px buffer
24
+ // appears below the border which in practice looks better:
25
+ $playground_section_separator_buffer: 1px !default;
26
+
27
+ html {
28
+ // Xcode (6.1.1 - 6A2008a) adds 3px to the font-size in HTML sections when
29
+ // compared against the font-size used in the swift code sections. The
30
+ // following 'calc' compensates for this, so 1 rem of Menlo in the HTML will
31
+ // be the exact same size as code in the swift sections (using the default
32
+ // Xcode themes that use the Menlo font):
33
+ font-size: calc(1em - #{$playground_font_adjustment}) ;
34
+ margin: 0;
35
+ padding: 0;
36
+ }
37
+
38
+ body {
39
+ position: relative;
40
+ overflow: hidden;
41
+ margin: 0;
42
+ box-sizing: border-box;
43
+
44
+ font-family: $playground_font_family;
45
+ font-size: $playground_font_size;
46
+
47
+ @if $playground_section_separator_buffer > 0 {
48
+ border-bottom: $playground_section_separator_buffer solid transparent;
49
+ }
50
+
51
+ background: transparent;
52
+
53
+ > section {
54
+ box-sizing: border-box;
55
+
56
+ padding: 0 ($playground_gutter_width + $playground_text_inset);
57
+ background: $playground_background_color;
58
+ border: $playground_section_separator_height solid $playground_section_separator_color;
59
+ border-width: $playground_section_separator_height 0;
60
+
61
+ @media (max-height: ($playground_section_separator_height * 2) + 1) {
62
+ border-bottom: none;
63
+ }
64
+ }
65
+
66
+ > .gutter {
67
+ display: block;
68
+ position: absolute;
69
+ left: 0;
70
+ top: $playground_section_separator_height;
71
+ bottom: $playground_section_separator_height;
72
+ width: $playground_gutter_width;
73
+ box-sizing: border-box;
74
+
75
+ background: $playground_gutter_color;
76
+
77
+ > .margin {
78
+ display: block;
79
+ position: absolute;
80
+ right: 0;
81
+ top: 0;
82
+ bottom: 0;
83
+ width: $playground_gutter_right_margin_inset;
84
+ box-sizing: border-box;
85
+
86
+ border-left: $playground_gutter_right_margin_line_width
87
+ $playground_gutter_right_margin_line_style
88
+ $playground_gutter_right_margin_line_color;
89
+ }
90
+ }
91
+ }
92
+
93
+ code, pre {
94
+ font-family: Menlo, "Andale Mono", Monaco, monospace;
95
+ font-size: 1rem;
96
+ }
@@ -0,0 +1,23 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Section <%= number %></title>
6
+ <% stylesheets.each_with_index do |stylesheet, index| %>
7
+ <link rel="stylesheet" type="text/css" href="<%= stylesheet.filename(index + 1) %>">
8
+ <% end %>
9
+ <% javascripts.each_with_index do |javascript, index| %>
10
+ <script src="<%= javascript.filename(index + 1) %>"></script>
11
+ <% end %>
12
+ <meta id="xcode-display" name="xcode-display" content="render">
13
+ <meta name="apple-mobile-web-app-capable" content="yes">
14
+ <meta name="viewport" content="width=device-width,maximum-scale=1.0">
15
+ </head>
16
+
17
+ <body>
18
+ <span class="gutter"><span class="margin"></span></span>
19
+ <section>
20
+ <%= content %>
21
+ </section>
22
+ </body>
23
+ </html>
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2
+ <playground version="3.0" sdk="<%= sdk %>" allows-reset="<%= allows_reset? ? 'YES' : 'NO' %>">
3
+ <sections>
4
+ <% sections.each_with_index do |section, index| %>
5
+ <%= section.xcplayground_node(index + 1).to_xml %>
6
+ <% end %>
7
+ </sections>
8
+ </playground>
@@ -0,0 +1,3 @@
1
+ require_relative 'util/syntax_highlighting'
2
+ require_relative 'util/pipeline'
3
+ require_relative 'util/path_or_content'
@@ -0,0 +1,31 @@
1
+ require 'pathname'
2
+
3
+ module Swift::Playground::Util
4
+ module PathOrContent
5
+ def path_or_content_as_io(path_or_content)
6
+ # Return path_or_content if it is an IO-like object
7
+ return path_or_content if path_or_content.respond_to?(:read)
8
+
9
+ unless path_or_content.is_a?(String)
10
+ raise "You must provide either a String or an IO object when constructing a #{self.class.name}."
11
+ end
12
+
13
+ if path_or_content !~ /[^\n]/ && !path_or_content.blank?
14
+ path = Pathname.new(path_or_content).expand_path
15
+ return path if path.exist?
16
+
17
+ raise "Path '#{path}' not found. Please add a newline to any raw content."
18
+ else
19
+ StringIO.new(path_or_content)
20
+ end
21
+ end
22
+
23
+ def derived_filename(pathname_or_content)
24
+ if pathname_or_content.respond_to?(:basename)
25
+ pathname_or_content.basename.to_s
26
+ elsif pathname_or_content.respond_to?(:path)
27
+ File.basename(pathname_or_content.path)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,53 @@
1
+ require 'html/pipeline'
2
+ require 'active_support/core_ext/object/deep_dup'
3
+
4
+ require_relative 'syntax_highlighting'
5
+ require_relative 'pipeline/section_filter'
6
+ require_relative 'pipeline/unicode_emoji_filter'
7
+
8
+ module Swift::Playground::Util
9
+ class Pipeline
10
+ HTMLWhitelist = HTML::Pipeline::SanitizationFilter::WHITELIST.deep_dup.tap do |whitelist|
11
+ # Allow <section> elements to have a 'role' attribute (which we use to
12
+ # distinguish between sections):
13
+ whitelist[:elements] << 'section'
14
+ whitelist[:attributes]['section'] = ['role']
15
+ end
16
+
17
+ MarkdownFilterChain = [
18
+ HTML::Pipeline::MarkdownFilter,
19
+
20
+ # Filter for splitting out resulting HTML into separate HTML and swift
21
+ # <section> elements, with appropriate metadata attached:
22
+ SectionFilter,
23
+
24
+ HTML::Pipeline::SanitizationFilter
25
+ ]
26
+
27
+ # Custom Emoji filter than replaces with unicode characters rather than
28
+ # images (because a Swift Playground will always be opened on OS X which
29
+ # supports rendering the unicode version natively):
30
+ EmojiFilter = UnicodeEmojiFilter
31
+
32
+ SyntaxHighlightFilter = (HTML::Pipeline::SyntaxHighlightFilter if SyntaxHighlighting.available?)
33
+
34
+ attr_accessor :filters
35
+
36
+ def initialize(filters = [])
37
+ self.filters = filters
38
+ end
39
+
40
+ def has_filters?
41
+ self.filters.any?
42
+ end
43
+
44
+ def call(html, context = {}, result = nil)
45
+ context = {
46
+ gfm: true, # Enable support for GitHub formatted Markdown
47
+ whitelist: HTMLWhitelist # Control HTML elements that are sanitized
48
+ }.merge(context)
49
+
50
+ HTML::Pipeline.new(filters.compact, context).call(html, context, result)
51
+ end
52
+ end
53
+ end