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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.ruby-version +1 -0
- data/Gemfile +19 -0
- data/LICENSE +22 -0
- data/README.md +209 -0
- data/Rakefile +16 -0
- data/bin/swift-playground +5 -0
- data/lib/swift/playground.rb +193 -0
- data/lib/swift/playground/asset.rb +52 -0
- data/lib/swift/playground/assets/javascript.rb +9 -0
- data/lib/swift/playground/assets/stylesheet.rb +36 -0
- data/lib/swift/playground/cli.rb +29 -0
- data/lib/swift/playground/cli/commands/generate.rb +95 -0
- data/lib/swift/playground/cli/commands/new.rb +49 -0
- data/lib/swift/playground/cli/definition.rb +32 -0
- data/lib/swift/playground/cli/global/error_handling.rb +34 -0
- data/lib/swift/playground/cli/shared_attributes.rb +19 -0
- data/lib/swift/playground/cli/ui.rb +120 -0
- data/lib/swift/playground/debug.rb +4 -0
- data/lib/swift/playground/generator.rb +32 -0
- data/lib/swift/playground/metadata.rb +12 -0
- data/lib/swift/playground/section.rb +117 -0
- data/lib/swift/playground/sections/code_section.rb +19 -0
- data/lib/swift/playground/sections/documentation_section.rb +63 -0
- data/lib/swift/playground/template/Documentation/defaults.css.scss +96 -0
- data/lib/swift/playground/template/Documentation/section.html.erb +23 -0
- data/lib/swift/playground/template/contents.xcplayground.erb +8 -0
- data/lib/swift/playground/util.rb +3 -0
- data/lib/swift/playground/util/path_or_content.rb +31 -0
- data/lib/swift/playground/util/pipeline.rb +53 -0
- data/lib/swift/playground/util/pipeline/section_filter.rb +55 -0
- data/lib/swift/playground/util/pipeline/unicode_emoji_filter.rb +27 -0
- data/lib/swift/playground/util/syntax_highlighting.rb +24 -0
- data/swift-playground.gemspec +29 -0
- metadata +207 -0
@@ -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,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
|