├── .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 |
20 | <%= content %> 21 |
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: '