Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ gem "mocha-on-bacon"
gem "prettybacon"
gem "fakefs"
gem "rubocop", "~> 0.41.2", require: false
gem "nokogiri"

require "pp" # https://github.com/defunkt/fakefs/issues/99
gemspec
6 changes: 6 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,21 @@ GEM
lumberjack (1.0.10)
metaclass (0.0.4)
method_source (0.8.2)
mini_portile2 (2.1.0)
mocha (1.1.0)
metaclass (~> 0.0.1)
mocha-on-bacon (0.2.2)
mocha (>= 0.13.0)
nenv (0.3.0)
nokogiri (1.6.8)
mini_portile2 (~> 2.1.0)
pkg-config (~> 1.1.7)
notiffany (0.1.0)
nenv (~> 0.1)
shellany (~> 0.0)
parser (2.3.1.2)
ast (~> 2.2)
pkg-config (1.1.7)
plist (3.2.0)
powerpack (0.1.1)
prettybacon (0.0.2)
Expand Down Expand Up @@ -96,6 +101,7 @@ DEPENDENCIES
guard-rspec
mocha
mocha-on-bacon
nokogiri
playgroundbook!
prettybacon
rake
Expand Down
48 changes: 3 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,61 +55,19 @@ Each chapter needs to have a corresponding playground; so `Chapter 1` requires t

Only the link to the term must be URL encoded. For example, the term "reuse identifier" would be defined in the yaml as `reuse identifier` but linked to as `glossary://reuse%20identifier`.

Each chapter needs to be in the following format:

```swift
// This is the preamble that is shared among all the pages within this chapter.

public var str = "Hi!"

public func sharedFunc() {
print("This should be accessible to all pages.")
}

//// Page 1

str = "Yo, it's page 1."
sharedFunc()

//// Page 2

sharedFunc()
str = "Page 2 awww yeah."
```

Pages are divided by lines beginning with a quadruple slash, followed by that pages name.
Each page in a chapter's `.playground` will be a separate page in the `.playgroundbook` and it's `Source`. The contents of the `Source` and `Resource` folders for each chapter and each page are copied.

### Limitations of Book Rendering

Playground books support a rich set of awesome features to make learning how to code really easy, and this tool only scratches the surface. Read over the [Playground Book reference](https://developer.apple.com/library/content/documentation/Xcode/Conceptual/swift_playgrounds_doc_format/) to see all the available options. If you have suggestions, please open an issue :+1:
=======
The preamble (anything about the first `////` page) is put in its own file. That means declarations there need to be `public` to be visible within individual pages (even though when you're writing, everything is in one file). Additionally, the preamble is at the top-level and can't contain expressions. This would cause a compiler error in the Swift Playrounds iPad app:

```swift
public let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: 20, height: 20)
```

Instead, you have to wrap it in a closure, like this:

```swift
public var layout: UICollectionViewFlowLayout = {
let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: 20, height: 20)
return layout
}()
```

It's awkward; if you have suggestions, open an issue :+1:

Sharing resources is only available book-wide and not specific to chapters. Sharing code outside the preamble isn't supported yet.

#### Using Swift Package Manager Dependencies

If you place a `Package.swift` file next to your playgroundbook manifest used for rendering a book and run `swift package fetch` to fetch the corresponding sources into `Packages/`, these will be copied into the playground book's top-level `Sources` when building and available in the finished book.
These source files will also be copied into the original playground for each chapter to make these available for use in Xcode.
Please note that this will work best with SPM packages containing pure Swift.

Playground books support a rich set of awesome features to make learning how to code really easy, and this tool uses almost none of them. It sacrifices this experience for the sake of being able to easily write the books on your Mac.

### Creating a Playground from markdown

Maybe you want to do something for a website, or a git repo first, and then generate your Playground? Well in those cases your source of truth is the markdown document. For that case, we have `playgroundbook wrapper`.
Expand Down
25 changes: 24 additions & 1 deletion lib/renderer/chapter_collator.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
require "plist"
require "fileutils"
require "renderer/page_writer"

module Playgroundbook
SharedSourcesDirectoryName = "Sources".freeze
SharedResourcesDirectoryName = "Resources".freeze
PreambleFileName = "Preamble.swift".freeze

class ChapterCollator
Expand All @@ -26,12 +28,17 @@ def collate(chapter, parsed_chapter, imports)
page_contents = parsed_chapter[:page_contents][index]
page_dir_name = parsed_chapter[:page_dir_names][index]

@page_writer.write_page(page_name, page_dir_name, imports, page_contents, chapter)
page_source_names = parsed_chapter[:page_source_names][index]
page_resource_names = parsed_chapter[:page_resource_names][index]

@page_writer.write_page(page_name, page_dir_name, imports, page_contents, page_source_names, page_resource_names, chapter)
end
end

write_chapter_manifest(chapter_name, parsed_chapter[:page_dir_names])
write_preamble(parsed_chapter[:preamble])
copy_sources(parsed_chapter[:source_names])
copy_resources(parsed_chapter[:resource_names])
end
end

Expand All @@ -56,5 +63,21 @@ def write_preamble(preamble)
end
end
end

def copy_sources(source_names)
Dir.mkdir(SharedSourcesDirectoryName) unless Dir.exist?(SharedSourcesDirectoryName)

source_names.each do |source|
FileUtils.cp("../../../../#{source}", SharedSourcesDirectoryName)
end
end

def copy_resources(resource_names)
Dir.mkdir(SharedResourcesDirectoryName) unless Dir.exist?(SharedResourcesDirectoryName)

resource_names.each do |resource|
FileUtils.cp("../../../../#{resource}", SharedResourcesDirectoryName)
end
end
end
end
26 changes: 24 additions & 2 deletions lib/renderer/page_parser.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Playgroundbook
class PageParser
def parse_chapter_pages(chapter_contents)
def parse_chapter_pages(chapter_contents, source_names, resource_names)
# Looks for //// PageName separators.
page_names = chapter_contents.scan(/\/\/\/\/.*$/).map { |p| p.gsub("////", "").strip }
page_dir_names = page_names.map { |p| "#{p}.playgroundpage" }
Expand All @@ -13,7 +13,29 @@ def parse_chapter_pages(chapter_contents)
page_dir_names: page_dir_names,
page_names: page_names,
page_contents: page_contents,
preamble: preamble
preamble: preamble,
page_source_names: [[]] * page_names.count, # TODO: Be less hacky
page_resource_names: [[]] * page_names.count, # TODO: Be less hacky
source_names: source_names,
resource_names: resource_names
}
end

def parse_chapter_xcplaygroundpages(pages_data, source_names, resource_names)
page_dir_names = pages_data.map { |p| "#{p[:name]}.playgroundpage" }
page_names = pages_data.map { |p| "#{p[:name]}" }
page_contents = pages_data.map { |p| "#{p[:contents]}" }
page_source_names = pages_data.map { |p| p[:sources] }
page_resource_names = pages_data.map { |p| p[:resources] }

{
page_dir_names: page_dir_names,
page_names: page_names,
page_contents: page_contents,
page_source_names: page_source_names,
page_resource_names: page_resource_names,
source_names: source_names,
resource_names: resource_names
}
end
end
Expand Down
21 changes: 20 additions & 1 deletion lib/renderer/page_writer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def initialize(page_processor = PageProcessor.new, ui = Cork::Board.new)
@ui = ui
end

def write_page(page_name, page_dir_name, imports, page_contents, chapter_info={})
def write_page(page_name, page_dir_name, imports, page_contents, page_sources, page_resources, chapter_info={})
Dir.mkdir(page_dir_name) unless Dir.exist?(page_dir_name)
contents_with_import = "//#-hidden-code\n"
contents_with_import += imports.map { |i| "import #{i}" }.join("\n") + "\n"
Expand All @@ -29,6 +29,25 @@ def write_page(page_name, page_dir_name, imports, page_contents, chapter_info={}
"ContentVersion" => "1.0"
}.to_plist)
end

copy_page_sources(page_sources)
copy_page_resources(page_resources)
end
end

def copy_page_sources(source_names)
Dir.mkdir(SharedSourcesDirectoryName) unless Dir.exist?(SharedSourcesDirectoryName)

source_names.each do |source|
FileUtils.cp("../../../../../../#{source}", SharedSourcesDirectoryName)
end
end

def copy_page_resources(resource_names)
Dir.mkdir(SharedResourcesDirectoryName) unless Dir.exist?(SharedResourcesDirectoryName)

resource_names.each do |resource|
FileUtils.cp("../../../../../../#{resource}", SharedResourcesDirectoryName)
end
end
end
Expand Down
30 changes: 25 additions & 5 deletions lib/renderer/playgroundbook_renderer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
require "pathname"
require "yaml"
require "fileutils"
require "nokogiri"
require "renderer/contents_manifest_generator"
require "renderer/chapter_collator"
require "renderer/page_parser"
Expand Down Expand Up @@ -39,17 +40,36 @@ def render

book = yaml_contents
book_dir_name = "#{book['name']}.playgroundbook"
book_chapter_contents = []
parsed_chapters = []
# TODO: Validate YAML contents?
begin
book_chapter_contents = book["chapters"].map do |chapter|
File.read("#{chapter['name']}.playground/Contents.swift")
parsed_chapters = book["chapters"].map do |chapter|
source_names = Dir["#{chapter['name']}.playground/Sources/*.swift"]
resource_names = Dir["#{chapter['name']}.playground/Resources/*"]
single_page_file = "#{chapter['name']}.playground/Contents.swift"
if File.exist?(single_page_file)
c = File.read(single_page_file)
page_parser.parse_chapter_pages(c, source_names, resource_names)
elsif !Dir.glob("#{chapter['name']}.playground/Pages/*.xcplaygroundpage").empty?
toc = Nokogiri::XML(File.read("#{chapter['name']}.playground/contents.xcplayground"))
page_names = toc.xpath("//page").map { |p| p["name"] }
pages_data = page_names.map do |p|
{
name: p,
contents: File.read("#{chapter['name']}.playground/Pages/#{p}.xcplaygroundpage/Contents.swift"),
sources: Dir["#{chapter['name']}.playground/Pages/#{p}.xcplaygroundpage/Sources/*.swift"],
resources: Dir["#{chapter['name']}.playground/Pages/#{p}.xcplaygroundpage/Resources/*"]
}
end
page_parser.parse_chapter_xcplaygroundpages(pages_data, source_names, resource_names)
else
raise "Missing valid playground for #{chapter['name']}."
end
end
rescue => e
ui.puts "Failed to open playground Contents.swift file."
ui.puts "Failed to open and parse playground chapter."
raise e
end
parsed_chapters = book_chapter_contents.map { |c| page_parser.parse_chapter_pages(c) }

book["chapters"].map do |chapter|
Dir.glob("Packages/**/Sources/*.swift").each do |file|
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file not shown.
8 changes: 4 additions & 4 deletions spec/renderer/chapter_collator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module Playgroundbook
let(:collator) { ChapterCollator.new(page_writer, test_ui) }
let(:page_writer) { double(PageWriter) }
let(:test_ui) { Cork::Board.new(silent: true) }
let(:parsed_chapter) { PageParser.new.parse_chapter_pages(test_chapter_contents) }
let(:parsed_chapter) { PageParser.new.parse_chapter_pages(test_chapter_contents, [], []) }
let(:chapter) { { 'name' => "test_chapter" } }

before do
Expand Down Expand Up @@ -37,8 +37,8 @@ module Playgroundbook
end

it "calls the page_writer for each page" do
expect(page_writer).to receive(:write_page).with("Page 1", "Page 1.playgroundpage", [], "str = \"Yo, it's page 1.\"\nsharedFunc()", {"name"=>"test_chapter"})
expect(page_writer).to receive(:write_page).with("Page 2", "Page 2.playgroundpage", [], "str = \"Page 2 awww yeah.\"\nsharedFunc()", {"name"=>"test_chapter"})
expect(page_writer).to receive(:write_page).with("Page 1", "Page 1.playgroundpage", [], "str = \"Yo, it's page 1.\"\nsharedFunc()", [], [], {"name"=>"test_chapter"})
expect(page_writer).to receive(:write_page).with("Page 2", "Page 2.playgroundpage", [], "str = \"Page 2 awww yeah.\"\nsharedFunc()", [], [], {"name"=>"test_chapter"})

collator.collate(chapter, parsed_chapter, [])
end
Expand All @@ -47,7 +47,7 @@ module Playgroundbook
expect { collator.collate(chapter, parsed_chapter, []) }.to_not raise_error
end

context "having colated" do
context "having collated" do
before do
collator.collate(chapter, parsed_chapter, [])
end
Expand Down
6 changes: 3 additions & 3 deletions spec/renderer/page_writer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,18 @@ module Playgroundbook
end

it "does not explode" do
expect { page_writer.write_page(page_name, page_dir_name, ["UIKit"], page_contents) }.to_not raise_error
expect { page_writer.write_page(page_name, page_dir_name, ["UIKit"], page_contents, [], []) }.to_not raise_error
end
end

it "calls the page processor" do
expect(page_processor).to receive(:strip_extraneous_newlines)
page_writer.write_page(page_name, page_dir_name, ["UIKit"], page_contents)
page_writer.write_page(page_name, page_dir_name, ["UIKit"], page_contents, [], [])
end

context "as a consequence of writing rendering" do
before do
page_writer.write_page(page_name, page_dir_name, ["UIKit"], page_contents)
page_writer.write_page(page_name, page_dir_name, ["UIKit"], page_contents, [], [])
end

it "creates a directory" do
Expand Down