├── .ruby-version
├── lib
└── swift
│ ├── playground
│ ├── debug.rb
│ ├── util.rb
│ ├── assets
│ │ ├── javascript.rb
│ │ └── stylesheet.rb
│ ├── template
│ │ ├── contents.xcplayground.erb
│ │ └── Documentation
│ │ │ ├── section.html.erb
│ │ │ └── defaults.css.scss
│ ├── sections
│ │ ├── code_section.rb
│ │ └── documentation_section.rb
│ ├── metadata.rb
│ ├── util
│ │ ├── source_io.rb
│ │ ├── syntax_highlighting.rb
│ │ ├── pipeline
│ │ │ ├── unicode_emoji_filter.rb
│ │ │ └── section_filter.rb
│ │ └── pipeline.rb
│ ├── cli.rb
│ ├── cli
│ │ ├── shared_attributes.rb
│ │ ├── global
│ │ │ └── error_handling.rb
│ │ ├── definition.rb
│ │ ├── commands
│ │ │ ├── new.rb
│ │ │ └── generate.rb
│ │ └── ui.rb
│ ├── generator.rb
│ ├── asset.rb
│ └── section.rb
│ └── playground.rb
├── bin
└── swift-playground
├── .gitignore
├── Rakefile
├── Gemfile
├── LICENSE.md
├── swift-playground.gemspec
└── README.md
/.ruby-version:
--------------------------------------------------------------------------------
1 | 2.0.0
2 |
--------------------------------------------------------------------------------
/lib/swift/playground/debug.rb:
--------------------------------------------------------------------------------
1 | begin
2 | require 'pry'
3 | rescue LoadError => e
4 | end
5 |
--------------------------------------------------------------------------------
/lib/swift/playground/util.rb:
--------------------------------------------------------------------------------
1 | require_relative 'util/syntax_highlighting'
2 | require_relative 'util/pipeline'
3 | require_relative 'util/source_io'
4 |
--------------------------------------------------------------------------------
/bin/swift-playground:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'swift/playground'
3 | require 'swift/playground/cli'
4 |
5 | exit Swift::Playground::CLI.run(ARGV)
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /Gemfile.lock
4 | /_yardoc/
5 | /coverage/
6 | /doc/
7 | /pkg/
8 | /spec/reports/
9 | /tmp/
10 | *.bundle
11 | *.so
12 | *.o
13 | *.a
14 | mkmf.log
15 |
--------------------------------------------------------------------------------
/lib/swift/playground/assets/javascript.rb:
--------------------------------------------------------------------------------
1 | require 'sass'
2 |
3 | module Swift
4 | class Playground
5 | class Javascript < Asset
6 | default_filename 'javascript-%d.js'
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/swift/playground/template/contents.xcplayground.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <% sections.each_with_index do |section, index| %>
5 | <%= section.xcplayground_node(index + 1).to_xml %>
6 | <% end %>
7 |
8 |
9 |
--------------------------------------------------------------------------------
/lib/swift/playground/sections/code_section.rb:
--------------------------------------------------------------------------------
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
20 |
--------------------------------------------------------------------------------
/lib/swift/playground/metadata.rb:
--------------------------------------------------------------------------------
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.5'
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | module Bundler
2 | class GemHelper
3 | def perform_git_push_with_clean_env(options = '')
4 | # Using a clean ENV ensures that ruby-based git credential helpers
5 | # such as that used by boxen will still work:
6 | Bundler.with_clean_env do
7 | perform_git_push_without_clean_env(options)
8 | end
9 | end
10 |
11 | alias_method :perform_git_push_without_clean_env, :perform_git_push
12 | alias_method :perform_git_push, :perform_git_push_with_clean_env
13 | end
14 | end
15 |
16 | require 'bundler/gem_tasks'
17 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gemspec
4 |
5 | gem 'rake'
6 |
7 | group :development do
8 | # github-linguist requires charlock_holmes which is not an easy install, making
9 | # this an optional dependency gives gem users the choice of whether to solve
10 | # the problem of installing that gem in order to get syntax highlighting.
11 | #
12 | # pygments.rb can also be problematic on some platforms and also is used
13 | # only for syntax highlighting so can be optional also:
14 | gem 'github-linguist', '~> 4.3.1'
15 | gem 'pygments.rb', '~> 0.6.0'
16 |
17 | gem 'pry'
18 | gem 'pry-byebug', '1.3.3'
19 | end
20 |
--------------------------------------------------------------------------------
/lib/swift/playground/util/source_io.rb:
--------------------------------------------------------------------------------
1 | require 'pathname'
2 |
3 | module Swift::Playground::Util
4 | module SourceIO
5 | def source_as_io(source)
6 | # Return path_or_content if it is an IO-like object
7 | return source if source.respond_to?(:read)
8 |
9 | unless source.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 | StringIO.new(source)
14 | end
15 |
16 | def derived_filename(source)
17 | if source.respond_to?(:basename)
18 | source.basename.to_s
19 | elsif source.respond_to?(:path)
20 | File.basename(source.path)
21 | end
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/swift/playground/cli.rb:
--------------------------------------------------------------------------------
1 | require 'gli'
2 | require_relative 'cli/definition'
3 | require_relative 'cli/shared_attributes'
4 | require_relative 'cli/ui'
5 | require_relative 'cli/commands/new'
6 | require_relative 'cli/commands/generate'
7 | require_relative 'cli/global/error_handling'
8 |
9 | require_relative 'generator'
10 |
11 | module Swift
12 | class Playground
13 | module CLI
14 | extend GLI::App
15 |
16 | program_desc SUMMARY
17 | version VERSION
18 |
19 | subcommand_option_handling :normal
20 | arguments :strict
21 | sort_help :manually
22 |
23 | include Commands::Generate
24 | include Commands::New
25 |
26 | include Global::ErrorHandling
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/lib/swift/playground/util/syntax_highlighting.rb:
--------------------------------------------------------------------------------
1 | begin
2 | require 'pygments'
3 | rescue LoadError
4 | # Ignore a failure to load the pygments gem
5 | end
6 |
7 | module Swift
8 | class Playground
9 | module Util
10 | class SyntaxHighlighting
11 | class << self
12 | def available?
13 | Gem::Specification::find_all_by_name('github-linguist').any? &&
14 | Gem::Specification::find_all_by_name('pygments.rb').any?
15 | end
16 |
17 | def css(style = 'default')
18 | if available?
19 | Pygments.css('.highlight', style: style)
20 | else
21 | ''
22 | end
23 | end
24 | end
25 | end
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/swift/playground/util/pipeline/unicode_emoji_filter.rb:
--------------------------------------------------------------------------------
1 | require 'html/pipeline'
2 |
3 | module Swift::Playground::Util
4 | class Pipeline
5 | class UnicodeEmojiFilter < HTML::Pipeline::EmojiFilter
6 |
7 | def validate
8 | # No need to for :asset_root in context like EmojiFilter requires
9 | end
10 |
11 | # Override EmojiFilter's image replacement to replace with Unicode instead:
12 | def emoji_image_filter(text)
13 | text.gsub(emoji_pattern) do |match|
14 | name = $1
15 | "#{emoji_unicode_replacement(name)}"
16 | end
17 | end
18 |
19 | private
20 |
21 | def emoji_unicode_replacement(name)
22 | Emoji.find_by_alias(name).raw
23 | end
24 |
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/swift/playground/cli/shared_attributes.rb:
--------------------------------------------------------------------------------
1 | module Swift::Playground::CLI
2 | module SharedCreationSwitches
3 | def self.extended(command)
4 | command.flag :platform,
5 | default_value: 'ios',
6 | arg_name: '[ios|osx]',
7 | must_match: %w{ios osx},
8 | desc: 'The target platform for the generated playground.'
9 |
10 | command.switch :reset,
11 | default_value: true,
12 | desc: 'Allow the playground to be reset to it\'s original state via "Editor > Reset Playground" in Xcode.'
13 |
14 | command.switch :open,
15 | negatable: false,
16 | desc: 'Open the playground in Xcode once it has been created.'
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/swift/playground/assets/stylesheet.rb:
--------------------------------------------------------------------------------
1 | require 'sass'
2 |
3 | module Swift
4 | class Playground
5 | class Stylesheet < Asset
6 | default_filename 'stylesheet-%d.css'
7 |
8 | def save(destination_path, number)
9 | save_content
10 |
11 | self.content = Sass.compile(content)
12 | super(destination_path, number)
13 |
14 | restore_content
15 | end
16 |
17 | protected
18 |
19 | def derived_filename(pathname_or_content)
20 | filename = super(pathname_or_content)
21 | filename.gsub(/\.scss$/, '') if filename
22 | end
23 |
24 | private
25 |
26 | def save_content
27 | @saved_content = content
28 | end
29 |
30 | def restore_content
31 | self.content = @saved_content
32 | @saved_content = nil
33 | end
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/lib/swift/playground/template/Documentation/section.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Section <%= number %>
6 | <% stylesheets.each_with_index do |stylesheet, index| %>
7 |
8 | <% end %>
9 | <% javascripts.each_with_index do |javascript, index| %>
10 |
11 | <% end %>
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/lib/swift/playground/generator.rb:
--------------------------------------------------------------------------------
1 | require_relative 'util'
2 |
3 | module Swift
4 | class Playground
5 | class Generator
6 | class << self
7 | include Util::SourceIO
8 |
9 | def generate(markdown, options={})
10 | markdown_file = source_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
33 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014-2015 [Resolve Digital](http://resolve.digital) and Mark Haylock
2 |
3 | MIT License
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/lib/swift/playground/cli/global/error_handling.rb:
--------------------------------------------------------------------------------
1 | module Swift::Playground::CLI
2 | module Global
3 | module ErrorHandling
4 | extend Definition
5 |
6 | definition do
7 | on_error do |exception|
8 | case exception
9 | when Interrupt
10 | UI.error
11 | UI.error("Execution interrupted.")
12 | when SystemExit
13 | # An intentional early exit has occurred and all relevant messages
14 | # have already been displayed, so do nothing
15 | else
16 | # We only want to display details of the exception under debug if it
17 | # is not a GLI exception (as a GLI exception relates to parsing
18 | # errors - e.g. wrong command, that we do not need to expand upon):
19 | debug_exception = (exception.class.to_s !~ /\AGLI/) ? exception : nil
20 |
21 | if exception.message
22 | UI.error("Execution failed: #{exception.message}", debug_exception)
23 | else
24 | UI.error("Execution failed.", debug_exception)
25 | end
26 | end
27 |
28 | false # Prevent default GLI error handling
29 | end
30 |
31 | end
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/lib/swift/playground/cli/definition.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/concern'
2 |
3 | # This class makes it possible to provide helper methods in a module that will
4 | # be included inside the main Swift::Playground::CLI module.
5 | #
6 | # It's unfortunately a little magical, but its difficult to work around this
7 | # due to the way the GLI dsl is not designed to use anywhere except at the top
8 | # level (or at best, inside a module) and not in a class.
9 | module Swift::Playground::CLI
10 | module Definition
11 | # Include ActiveSupport::Concern methods, so this module behaves like
12 | # ActiveSupport::Concern for any other module or class that extends it:
13 | include ActiveSupport::Concern
14 |
15 | def self.extended(mod)
16 | # Use the behaviour of the ActiveSupport::Concern modules `self.extended`
17 | # implementation:
18 | ActiveSupport::Concern.extended(mod)
19 | end
20 |
21 | def definition(&block)
22 | self.included do
23 | # The use of `extend(self)` here makes sure that a module that extends
24 | # the Definition module will have access to its methods from within
25 | # the GLI command actions it defines. It will define these commands
26 | # inside the block it passes to its call of the `definition` method.
27 | extend(self)
28 | self.class_eval(&block)
29 | end
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/swift-playground.gemspec:
--------------------------------------------------------------------------------
1 | # Ensure we require the local version and not one we might have installed already
2 | require File.join([File.dirname(__FILE__), 'lib', 'swift', 'playground', 'metadata.rb'])
3 | spec = Gem::Specification.new do |s|
4 | s.name = 'swift-playground'
5 | s.version = Swift::Playground::VERSION
6 | s.authors = ['Mark Haylock']
7 | s.email = ['mark@resolvedigital.co.nz']
8 | s.homepage = 'https://github.com/resolve/swift-playground'
9 | s.license = 'MIT'
10 | s.summary = Swift::Playground::SUMMARY
11 | s.description = Swift::Playground::DESCRIPTION
12 |
13 | s.files = `git ls-files -z`.split("\x0")
14 | s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
15 | s.test_files = s.files.grep(%r{^(test|spec|features)/})
16 | s.require_paths = ['lib']
17 |
18 | s.required_ruby_version = '>= 2.0.0'
19 |
20 | s.add_runtime_dependency 'html-pipeline', '~> 1.11'
21 | s.add_runtime_dependency 'activesupport', '~> 4.0'
22 | s.add_runtime_dependency 'github-markdown', '~> 0.6.7'
23 | s.add_runtime_dependency 'sanitize', '~> 3.0'
24 | s.add_runtime_dependency 'gemoji', '~> 2.1.0'
25 | s.add_runtime_dependency 'gli', '~> 2.12.2'
26 | s.add_runtime_dependency 'paint', '~> 0.9.0'
27 | s.add_runtime_dependency 'highline', '~> 1.6.21'
28 | s.add_runtime_dependency 'sass', '~> 3.2'
29 | end
30 |
--------------------------------------------------------------------------------
/lib/swift/playground/asset.rb:
--------------------------------------------------------------------------------
1 | module Swift
2 | class Playground
3 | assets_path = Pathname.new('swift/playground/assets')
4 | autoload :Stylesheet, assets_path.join('stylesheet')
5 | autoload :Javascript, assets_path.join('javascript')
6 |
7 | class Asset
8 | include Util::SourceIO
9 |
10 | class << self
11 | protected
12 |
13 | def default_filename(filename = nil)
14 | @default_filename = filename unless filename.nil?
15 | @default_filename
16 | end
17 | end
18 |
19 | attr_accessor :content, :filename
20 |
21 | def initialize(content, options = {})
22 | pathname_or_content = source_as_io(content)
23 | self.content = pathname_or_content.read
24 |
25 | filename = options[:filename] || derived_filename(pathname_or_content)
26 | @filename = filename || default_filename
27 | end
28 |
29 | def filename(number)
30 | @filename % number
31 | end
32 |
33 | def save(destination_path, number)
34 | destination_path = Pathname.new(destination_path)
35 |
36 | expanded_filename = filename(number)
37 | path = destination_path.join(expanded_filename)
38 |
39 | FileUtils.mkdir_p path.dirname
40 | path.open('w') do |file|
41 | file.write content
42 | end
43 | end
44 |
45 | protected
46 |
47 | def default_filename
48 | self.class.send(:default_filename)
49 | end
50 | end
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/lib/swift/playground/cli/commands/new.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/core_ext/string/strip'
2 |
3 | module Swift::Playground::CLI
4 | module Commands
5 | module New
6 | extend Definition
7 |
8 | definition do
9 | desc 'Create an empty playground (just as Xcode would via "File > New > Playground...")'
10 | arg ''
11 | command :new do |c|
12 | c.extend SharedCreationSwitches
13 |
14 | c.action do |_, options, args|
15 | playground_file = Pathname.new(args[0]).expand_path
16 |
17 | playground = Swift::Playground.new(platform: options[:platform])
18 |
19 | case options[:platform]
20 | when 'ios'
21 | contents = <<-IOS.strip_heredoc
22 | // Playground - noun: a place where people can play
23 |
24 | import UIKit
25 |
26 | var str = "Hello, playground"
27 | IOS
28 | when 'osx'
29 | contents = <<-OSX.strip_heredoc
30 | // Playground - noun: a place where people can play
31 |
32 | import Cocoa
33 |
34 | var str = "Hello, playground"
35 | OSX
36 | end
37 |
38 | playground.sections << Swift::Playground::CodeSection.new(contents)
39 | playground.save(playground_file)
40 |
41 | if options['open']
42 | system('open', playground_file.to_s)
43 | end
44 | end
45 | end
46 | end
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/lib/swift/playground/util/pipeline.rb:
--------------------------------------------------------------------------------
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 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 | # 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
54 |
--------------------------------------------------------------------------------
/lib/swift/playground/sections/documentation_section.rb:
--------------------------------------------------------------------------------
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 =~ /(]/
16 | raise 'Please provide an HTML fragment only. ' +
17 | 'Do not include an , or 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
64 |
--------------------------------------------------------------------------------
/lib/swift/playground/util/pipeline/section_filter.rb:
--------------------------------------------------------------------------------
1 | require 'html/pipeline'
2 |
3 | module Swift::Playground::Util
4 | class Pipeline
5 | class SectionFilter < HTML::Pipeline::Filter
6 | def call
7 | # Solution derived from http://stackoverflow.com/a/4799902
8 | children = doc.children # Every immediate child of the doc
9 | doc.inner_html = '' # Empty the doc now that we have our nodes
10 |
11 | # Comments preceding a swift code section can have meaning, so we need
12 | # to track the last comment made:
13 | last_comment = nil
14 | section = new_section(doc) # Create our first container in the doc
15 | children.each do |node|
16 | if node.name == 'comment'
17 | last_comment = node.content.strip
18 | elsif node.name == 'pre' && node[:lang] == 'swift' && last_comment != 'IGNORE'
19 | # If this code is the first thing in the document then the previous
20 | # section will be empty and the only child of the document, so we
21 | # should remove it:
22 | section.remove if section.content.empty? && doc.children.count == 1
23 |
24 | swift_section = new_section(doc, role: 'code')
25 | swift_section[:title] = last_comment unless last_comment.blank?
26 | node.remove_attribute('lang')
27 | swift_section << node
28 |
29 | section = new_section(doc) # Create a new container for subsequent nodes
30 | else
31 | last_comment = nil unless node.name == 'text' && node.content.blank?
32 | section << node
33 | end
34 | end
35 | section.remove if section.content.empty? # Get rid of a trailing, empty section
36 |
37 | doc
38 | end
39 |
40 | private
41 |
42 | def new_section(doc, attributes = {})
43 | attributes = {
44 | role: 'documentation'
45 | }.merge(attributes)
46 |
47 | section = (doc << '').children.last
48 | attributes.each do |attribute, value|
49 | section[attribute] = value
50 | end
51 | section
52 | end
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/lib/swift/playground/cli/ui.rb:
--------------------------------------------------------------------------------
1 | require 'highline/import'
2 | require 'paint'
3 | require 'forwardable'
4 | require 'active_support/core_ext/module'
5 |
6 | unless STDOUT.tty?
7 | # If we aren't using a TTY, then we need to avoid Highline attempting to set
8 | # TTY specific features (such as 'no echo mode'), as these will fail. We can
9 | # do so by monkey patching Highline to make the methods that peform these
10 | # functions no-ops:
11 | class HighLine
12 | module SystemExtensions
13 | def raw_no_echo_mode
14 | end
15 |
16 | def restore_mode
17 | end
18 | end
19 | end
20 | end
21 |
22 | Paint::SHORTCUTS[:swift_playground] = {
23 | :red => Paint.color(:red),
24 | :blue => Paint.color(:blue),
25 | :cyan => Paint.color(:cyan),
26 | :bright => Paint.color(:bright)
27 | }
28 | $paint = Paint::SwiftPlayground
29 |
30 | # Convenience module for accessing Highline features
31 | module Swift::Playground::CLI
32 | module UI
33 | extend SingleForwardable
34 |
35 | mattr_accessor :show_debug, :color_mode, :silence
36 |
37 | def_delegators :$terminal, :agree, :ask, :choose
38 | def_delegators :$paint, *Paint::SHORTCUTS[:swift_playground].keys
39 |
40 | class << self
41 | def say(message = "\n")
42 | return if silence
43 |
44 | terminal.say message
45 | end
46 |
47 | def error(message = nil, exception = nil)
48 | return if silence
49 |
50 | stderr.puts red(message)
51 | if exception && show_debug
52 | exception_details = ["Handled <#{exception.class}>:",
53 | exception.message,
54 | *exception.backtrace]
55 | stderr.puts red("\n" + exception_details.join("\n") + "\n")
56 | end
57 | end
58 |
59 | def debug(message = nil, &block)
60 | return if silence
61 |
62 | if show_debug
63 | message = formatted_log_message(message, &block)
64 | stderr.puts blue(message)
65 | end
66 | end
67 |
68 | def info(message = nil, &block)
69 | return if silence
70 |
71 | if show_debug
72 | message = formatted_log_message(message, &block)
73 | stderr.puts cyan(message)
74 | end
75 | end
76 |
77 | private
78 |
79 | def formatted_log_message(message = nil, &block)
80 | if block
81 | lines = block.call.split("\n")
82 | if message
83 | message = "#{message}: "
84 | message += "\n " if lines.count > 1
85 | message += lines.join("\n ")
86 | else
87 | message += lines.join("\n")
88 | end
89 | end
90 |
91 | message
92 | end
93 |
94 | def colorize(stream)
95 | case (color_mode || 'auto')
96 | when 'auto'
97 | if stream.tty?
98 | Paint.mode = Paint.detect_mode
99 | else
100 | Paint.mode = 0
101 | end
102 | when 'always'
103 | Paint.mode = Paint.detect_mode
104 | when 'never'
105 | Paint.mode = 0
106 | end
107 | end
108 |
109 | def terminal
110 | colorize($stdout)
111 | $terminal
112 | end
113 |
114 | def stderr
115 | colorize($stderr)
116 | $stderr
117 | end
118 | end
119 | end
120 | end
121 |
--------------------------------------------------------------------------------
/lib/swift/playground/section.rb:
--------------------------------------------------------------------------------
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::SourceIO
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 = source_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
118 |
--------------------------------------------------------------------------------
/lib/swift/playground/template/Documentation/defaults.css.scss:
--------------------------------------------------------------------------------
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 | }
97 |
--------------------------------------------------------------------------------
/lib/swift/playground/cli/commands/generate.rb:
--------------------------------------------------------------------------------
1 | module Swift::Playground::CLI
2 | module Commands
3 | module Generate
4 | extend Definition
5 |
6 | definition do
7 | desc 'Generate a playground file from the provided Markdown file'
8 | arg ''
9 | arg '', :optional
10 | command :generate do |c|
11 | c.extend SharedCreationSwitches
12 |
13 | c.flag :stylesheet,
14 | arg_name: '',
15 | type: String,
16 | desc: 'CSS stylesheet for the HTML documentation sections of the playground. SASS/SCSS syntax is supported. This will be included after the default stylesheet.'
17 |
18 | c.flag :javascript,
19 | arg_name: '',
20 | type: String,
21 | desc: 'A javascript file for the HTML documentation sections of the playground. Each section is rendered independently of another and the script will not have access to the DOM from any other sections.'
22 |
23 | c.switch :emoji,
24 | default_value: true,
25 | desc: "Convert emoji aliases (e.g. `:+1:`) into emoji characters."
26 |
27 | c.switch :highlighting,
28 | default_value: true,
29 | desc: "Detect non-swift code blocks and add syntax highlighting. Only has an effect if 'github-linguist' and 'pygments.rb' gems are installed."
30 |
31 | c.flag :'highlighting-style',
32 | arg_name: '