diff --git a/.github/workflows/json-syntax.yml b/.github/workflows/json-syntax.yml new file mode 100644 index 0000000..5d34ca1 --- /dev/null +++ b/.github/workflows/json-syntax.yml @@ -0,0 +1,15 @@ +name: JSON Syntax Check + +on: + pull_request: + paths: + - '**/*.json' + +jobs: + json-syntax: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check JSON syntax + run: | + find . -name '*.json' | xargs -I {} jq . {} > /dev/null \ No newline at end of file diff --git a/.github/workflows/ruby-syntax.yml b/.github/workflows/ruby-syntax.yml new file mode 100644 index 0000000..c888d3f --- /dev/null +++ b/.github/workflows/ruby-syntax.yml @@ -0,0 +1,19 @@ +name: Ruby Syntax Check + +on: + pull_request: + paths: + - '**/*.rb' + +jobs: + ruby-syntax: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.2' + - name: Check Ruby syntax + run: | + find . -name '*.rb' -exec ruby -c {} \; \ No newline at end of file diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..a4f52a5 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.2.0 \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2dd94cf --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,19 @@ +## Ruby Primer Agent Instructions + +### Build, Lint, and Test + +- **Testing**: This project uses RSpec. + - Run all tests: `bundle exec rspec` + - Run a single test file: `bundle exec rspec examples_spec/0.0.2.rb` +- **Linting**: There is no linting configuration. Please adhere to the style of existing code. + +### Code Style + +- **Formatting**: Use 2-space indentation. +- **Naming**: Use `snake_case` for variables and methods. +- **Strings**: Use double quotes for strings, especially with interpolation. Use single quotes for `require` statements. +- **Methods**: Use parentheses for method definitions with arguments. +- **Error Handling**: Not specified. +- **Imports**: Use `require`. +- **Types**: Not specified. +- **General**: Follow existing patterns in the code. Explicit `return` is preferred. diff --git a/Gemfile b/Gemfile index d2ecdb6..bfae0fb 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,4 @@ source 'https://rubygems.org' -gem 'colorize' -gem 'figlet' -gem 'rspec' -gem 'json' +gem 'colorize', '~> 1.1.0' +gem 'ruby_figlet', '~> 0.6' +gem 'rspec', '~> 3.13.0' diff --git a/examples_spec/0.0.2.rb b/examples_spec/0.0.2.rb index 8c67d01..a606d98 100644 --- a/examples_spec/0.0.2.rb +++ b/examples_spec/0.0.2.rb @@ -1,9 +1,9 @@ require 'rspec' describe "example code" do - let(:output) { `#{RbConfig.ruby} __TMPFILE__`.chomp } + let(:output) { `#{RbConfig.ruby} __TMPFILE__`.chomp } - it "results in 3" do - expect(output).to eq("3") - end + it "results in 3" do + expect(output).to eq("3") + end end diff --git a/examples_spec/0.1.2.rb b/examples_spec/0.1.2.rb index bf248e5..9c87669 100644 --- a/examples_spec/0.1.2.rb +++ b/examples_spec/0.1.2.rb @@ -2,29 +2,29 @@ describe "example code" do - let(:output) { `#{RbConfig.ruby} __TMPFILE__`.chomp } - let(:rspec_methods) { [ - :as_null_object, - :null_object?, - :pretty_inspect, - :pretty_print, - :pretty_print_cycle, - :pretty_print_inspect, - :pretty_print_instance_variables, - :received_message?, - :should, - :should_not, - :should_not_receive, - :should_receive, - :stub, - :stub_chain, + let(:output) { `#{RbConfig.ruby} __TMPFILE__`.chomp } + let(:rspec_methods) { [ + :as_null_object, + :null_object?, + :pretty_inspect, + :pretty_print, + :pretty_print_cycle, + :pretty_print_inspect, + :pretty_print_instance_variables, + :received_message?, + :should, + :should_not, + :should_not_receive, + :should_receive, + :stub, + :stub_chain, :to_json, - :unstub, - ] - } - let(:sorted_methods) { 1.methods.sort - rspec_methods } + :unstub, + ] + } + let(:sorted_methods) { 1.methods.sort - rspec_methods } - it "should sort the method list" do - expect(eval(output)).to eq(sorted_methods) - end + it "should sort the method list" do + expect(eval(output)).to eq(sorted_methods) + end end diff --git a/examples_spec/0.1.4.rb b/examples_spec/0.1.4.rb index 107a648..23f212e 100644 --- a/examples_spec/0.1.4.rb +++ b/examples_spec/0.1.4.rb @@ -1,9 +1,9 @@ require 'rspec' describe "example code" do - let(:output) { `#{RbConfig.ruby} __TMPFILE__`.chomp } + let(:output) { `#{RbConfig.ruby} __TMPFILE__`.chomp } - it "should check if two is between one and three" do - expect(eval(output)).to eq(2.between?(1,3)) - end + it "should check if two is between one and three" do + expect(eval(output)).to eq(2.between?(1,3)) + end end diff --git a/examples_spec/0.2.1.rb b/examples_spec/0.2.1.rb index 6ad27fe..63bf6c9 100644 --- a/examples_spec/0.2.1.rb +++ b/examples_spec/0.2.1.rb @@ -1,9 +1,9 @@ require 'rspec' describe "example code" do - let(:output) { `#{RbConfig.ruby} __TMPFILE__`.chomp } + let(:output) { `#{RbConfig.ruby} __TMPFILE__`.chomp } - it "should evaluate to seven" do - expect(output).to eq("7") - end + it "should evaluate to seven" do + expect(output).to eq("7") + end end diff --git a/examples_spec/1.0.0.rb b/examples_spec/1.0.0.rb index c7746f0..ef0196f 100644 --- a/examples_spec/1.0.0.rb +++ b/examples_spec/1.0.0.rb @@ -1,13 +1,13 @@ require 'rspec' describe "example code" do - let(:output) { `#{RbConfig.ruby} __TMPFILE__`.chomp } + let(:output) { `#{RbConfig.ruby} __TMPFILE__`.chomp } - it "should be a string" do - expect(output.is_a?(String)).to be(true) - end + it "should be a string" do + expect(output.is_a?(String)).to be(true) + end - it "should equal 'RubyMonk'" do - expect(output).to eq("RubyMonk") - end + it "should equal 'RubyMonk'" do + expect(output).to eq("RubyMonk") + end end diff --git a/examples_spec/1.0.1.rb b/examples_spec/1.0.1.rb index 3320a04..ffcc451 100644 --- a/examples_spec/1.0.1.rb +++ b/examples_spec/1.0.1.rb @@ -2,9 +2,9 @@ require 'date' describe "example code" do - let(:output) { `#{RbConfig.ruby} __TMPFILE__`.chomp } + let(:output) { `#{RbConfig.ruby} __TMPFILE__`.chomp } - it "should equal the current month" do - expect(output).to eq(Date.today.strftime("%B")) - end + it "should equal the current month" do + expect(output).to eq(Date.today.strftime("%B")) + end end diff --git a/examples_spec/1.0.2.rb b/examples_spec/1.0.2.rb index 3320a04..ffcc451 100644 --- a/examples_spec/1.0.2.rb +++ b/examples_spec/1.0.2.rb @@ -2,9 +2,9 @@ require 'date' describe "example code" do - let(:output) { `#{RbConfig.ruby} __TMPFILE__`.chomp } + let(:output) { `#{RbConfig.ruby} __TMPFILE__`.chomp } - it "should equal the current month" do - expect(output).to eq(Date.today.strftime("%B")) - end + it "should equal the current month" do + expect(output).to eq(Date.today.strftime("%B")) + end end diff --git a/examples_spec/1.0.3.rb b/examples_spec/1.0.3.rb index dfd3449..18eac70 100644 --- a/examples_spec/1.0.3.rb +++ b/examples_spec/1.0.3.rb @@ -2,9 +2,9 @@ require 'date' describe "example code" do - let(:output) { `#{RbConfig.ruby} __TMPFILE__`.chomp } + let(:output) { `#{RbConfig.ruby} __TMPFILE__`.chomp } - it "should equal the length of RubyMonk" do - expect(output.to_i).to eq("RubyMonk".length) - end + it "should equal the length of RubyMonk" do + expect(output.to_i).to eq("RubyMonk".length) + end end diff --git a/initRubyGems.sh b/initRubyGems.sh index 184754c..af60699 100755 --- a/initRubyGems.sh +++ b/initRubyGems.sh @@ -1,3 +1,3 @@ #!/bin/sh -bundle install --path vendor/bundle +/opt/homebrew/opt/ruby@3.2/bin/bundle install --path vendor/bundle diff --git a/lessons/0.0.json b/lessons/0.0.json index dcb7b63..c49a601 100644 --- a/lessons/0.0.json +++ b/lessons/0.0.json @@ -1,73 +1,78 @@ { - "subsections": [{ - "name": "Everything is an object", - "items": [{ - "type": "paragraph", - "value": "We will begin our journey with objects." + "subsections": [ + { + "name": "Everything Is An Object", + "items": [ + { + "type": "paragraph", + "value": "We will begin our journey with objects." }, { - "type": "paragraph", - "value": "In Ruby, just like in real life, our world is filled with objects. Everything is an object - integers, characters, text, arrays - everything." + "type": "paragraph", + "value": "In Ruby, just like in real life, our world is filled with objects. Everything is an object - integers, characters, text, arrays - everything." }, { - "type": "paragraph", - "value": "To make things happen using Ruby, one always puts oneself in the place of an object and then has conversations with other objects, telling them to do stuff." + "type": "paragraph", + "value": "To make things happen using Ruby, one always puts oneself in the place of an object and then has conversations with other objects, telling them to do stuff." }, { - "type": "paragraph", - "value": "Roleplaying as an object in your program is an integral part of object-oriented programming. To know which object you are at the moment, one may use the keyword 'self'." + "type": "paragraph", + "value": "Roleplaying as an object in your program is an integral part of object-oriented programming. To know which object you are at the moment, one may use the keyword `self`." }, { - "type": "example", - "value": "0.0.0" + "type": "example", + "value": "0.0.0" }, { - "type": "press_enter" + "type": "press_enter" }, { - "type": "paragraph", - "value": "As you can see, if you don't specify which object you are, you automatically play the role of the 'main' object that Ruby provides us by default." + "type": "paragraph", + "value": "As you can see, if you don't specify which object you are, you automatically play the role of the `main` object that Ruby provides us by default." }, { - "type": "paragraph", - "value": "We'll delve into how one can play the role of different objects and why this is useful a little further down the line." + "type": "paragraph", + "value": "We'll delve into how one can play the role of different objects and why this is useful a little further down the line." }, { - "type": "press_enter" - }] + "type": "press_enter" + } + ] }, { - "name": "Talking to objects", - "items": [{ - "type": "paragraph", - "value": "One object interacts with another by using what are called 'methods'. More specifically, one object 'calls' or 'invokes the methods' of another object." + "name": "Talking to Objects", + "items": [ + { + "type": "paragraph", + "value": "One object interacts with another by using what are called 'methods'. More specifically, one object 'calls' or 'invokes the methods' of another object." }, { - "type": "paragraph", - "value": "In the example below, we call the method 'even?' on the object that is the number '2' by placing a period (.) after the object, then adding in the name of the method we want to invoke." + "type": "paragraph", + "value": "In the example below, we call the method `even?` on the object that is the number `2` by placing a period (`.`) after the object, then adding in the name of the method we want to invoke." }, { - "type": "example", - "value": "0.0.1" + "type": "example", + "value": "0.0.1" }, { - "type": "press_enter" + "type": "press_enter" }, { - "type": "paragraph", - "value": "Invoking a method on an object inevitably generates a response. This response is always another object. Calling the method 'next' on the object '1' has it give us the next consecutive value, '2'." + "type": "paragraph", + "value": "Invoking a method on an object inevitably generates a response. This response is always another object. Calling the method `next` on the object `1` has it give us the next consecutive value, `2`." }, { - "type": "paragraph", - "value": "One may also chain method invocations by simply adding more periods and method names sequentially - each method in the chain is called on the result of the previous method. Go on and try it by invoking 'next' twice on '1' to get '3'." + "type": "paragraph", + "value": "One may also chain method invocations by simply adding more periods and method names sequentially - each method in the chain is called on the result of the previous method. Go on and try it by invoking `next` twice on `1` to get `3`." }, { - "type": "test_example", - "value": "0.0.2" + "type": "test_example", + "value": "0.0.2" }, { - "type": "press_enter" - }] - }] -} - + "type": "press_enter" + } + ] + } + ] +} \ No newline at end of file diff --git a/lessons/0.1.json b/lessons/0.1.json index ba74f13..1ef8a1a 100644 --- a/lessons/0.1.json +++ b/lessons/0.1.json @@ -1,68 +1,74 @@ { - "subsections": [{ - "name": "Looking up methods", - "items": [{ - "type": "header", - "value": "Looking up methods" + "subsections": [ + { + "name": "Looking Up Methods", + "items": [ + { + "type": "header", + "value": "Looking up methods" }, { - "type": "paragraph", - "value": "Ruby objects are happy to tell you what methods they provide. You simply call the 'methods' method on them." + "type": "paragraph", + "value": "Ruby objects are happy to tell you what methods they provide. You simply call the `methods` method on them." }, { - "type": "example", - "value": "0.1.1" - }, + "type": "example", + "value": "0.1.1" + }, { - "type": "press_enter" + "type": "press_enter" }, { - "type": "paragraph", - "value": "As you can see, you get a listing of all the methods on the number '1' that you could invoke. The names are prefixed with a colon (:) that you can safely ignore for now. If you find the results too muddled, you can easily sort them alphabetically. Try it for yourself - simply call the method 'sort' on the result of 'methods':" + "type": "paragraph", + "value": "As you can see, you get a listing of all the methods on the number `1` that you could invoke. The names are prefixed with a colon (`:`) that you can safely ignore for now. If you find the results too muddled, you can easily sort them alphabetically. Try it for yourself - simply call the method `sort` on the result of `methods`:" }, { - "type": "test_example", - "value": "0.1.2" + "type": "test_example", + "value": "0.1.2" }, { - "type": "press_enter" - }] + "type": "press_enter" + } + ] }, { - "name": "Invoking methods with arguments", - "items": [{ - "type": "paragraph", - "value": "When talking to an object via its methods, it is possible to give it additional information so it can give you an appropriate response." + "name": "Invoking Methods with Arguments", + "items": [ + { + "type": "paragraph", + "value": "When talking to an object via its methods, it is possible to give it additional information so it can give you an appropriate response." }, { - "type": "paragraph", - "value": "This additional information is called the 'arguments to a method'. The name 'argument' makes sense if you stop to think about the fact that methods are the paths of communication between objects." + "type": "paragraph", + "value": "This additional information is called the 'arguments to a method'. The name 'argument' makes sense if you stop to think about the fact that methods are the paths of communication between objects." }, { - "type": "paragraph", - "value": "Here's an example of an argument to the method 'index', which finds the position of the argument in the array:" + "type": "paragraph", + "value": "Here's an example of an argument to the method `index`, which finds the position of the argument in the array:" }, { - "type": "example", - "value": "0.1.3" + "type": "example", + "value": "0.1.3" }, { - "type": "press_enter" + "type": "press_enter" }, { - "type": "paragraph", - "value": "Here, 'index' is the method and 'paper' the argument. If there is more than one argument, they can be passed to the method by simply separating them with commas." + "type": "paragraph", + "value": "Here, `index` is the method and `paper` the argument. If there is more than one argument, they can be passed to the method by simply separating them with commas." }, { - "type": "paragraph", - "value": "Try using a method that takes two arguments - use the 'between?' method to determine if the number '2' lies between the numbers '1' and '3'." + "type": "paragraph", + "value": "Try using a method that takes two arguments - use the `between?` method to determine if the number `2` lies between the numbers `1` and `3`." }, { - "type": "test_example", - "value": "0.1.4" + "type": "test_example", + "value": "0.1.4" }, { - "type": "press_enter" - }] - }] -} + "type": "press_enter" + } + ] + } + ] +} \ No newline at end of file diff --git a/lessons/0.2.json b/lessons/0.2.json index d91ad67..d83cdb4 100644 --- a/lessons/0.2.json +++ b/lessons/0.2.json @@ -1,69 +1,73 @@ { - "subsections": [{ - "name": "Syntactic Sugar for Special Methods", - "items": [{ - "type": "paragraph", - "value": "As an observant initiate, you've probably noticed that in the last lesson, Integer objects list mathematical operators like '+' and '-' amont their methods. You probably also thought to yourself that invoking the '+' method like so - '1.+(2)' - to add two numbers would be ... clumsy." + "subsections": [ + { + "name": "Syntactic Sugar for Special Methods", + "items": [ + { + "type": "paragraph", + "value": "As an observant initiate, you've probably noticed that in the last lesson, `Integer` objects list mathematical operators like `+` and `-` amont their methods. You probably also thought to yourself that invoking the `+` method like so - `1.+(2)` - to add two numbers would be ... clumsy." }, { - "type": "paragraph", - "value": "It is, though it works just fine - try for yourself by adding '4' to '3' in the exercise below." + "type": "paragraph", + "value": "It is, though it works just fine - try for yourself by adding `4` to `3` in the exercise below." }, { - "type": "test_example", - "value": "0.2.1" + "type": "test_example", + "value": "0.2.1" }, { - "type": "press_enter" + "type": "press_enter" }, { - "type": "paragraph", - "value": "Ruby as a language aims to be extremely programmer-friendly, so you can usually safely assume that there is a better way. Ruby makes an exception in its syntactic rules for commonly used operators so you don't have to use periods to invoke them on object." + "type": "paragraph", + "value": "Ruby as a language aims to be extremely programmer-friendly, so you can usually safely assume that there is a better way. Ruby makes an exception in its syntactic rules for commonly used operators so you don't have to use periods to invoke them on object." }, { - "type": "paragraph", - "value": "Let us rephrase the previous example in a more natural syntax by omitting the periods and brackets." + "type": "paragraph", + "value": "Let us rephrase the previous example in a more natural syntax by omitting the periods and brackets." }, { - "type": "example", - "value": "0.2.2" + "type": "example", + "value": "0.2.2" }, { - "type": "press_enter" + "type": "press_enter" }, { - "type": "paragraph", - "value": "There are several other method names that have this special status - here's a quick summary of the ones you're most likely to run into." + "type": "paragraph", + "value": "There are several other method names that have this special status - here's a quick summary of the ones you're most likely to run into." }, { - "type": "paragraph", - "value": " + - * / = == != > < >= <= []" + "type": "paragraph", + "value": " `+` `-` `*` `/` `=` `==` `!=` `>` `<` `>=` `<=` `[]`" }, { - "type": "paragraph", - "value": "This last method ('[]') you've probably already seen in the lesson that covers Arrays, and is arguably the most unique in its syntax. Not only does it not require a period, it also encloses the arguments to itself. Here's a quick example to refresh your memory." + "type": "paragraph", + "value": "This last method (`[]`) you've probably already seen in the lesson that covers Arrays, and is arguably the most unique in its syntax. Not only does it not require a period, it also encloses the arguments to itself. Here's a quick example to refresh your memory." }, { - "type": "example", - "value": "0.2.3" + "type": "example", + "value": "0.2.3" }, { - "type": "paragraph", - "value": "Even more interesting is that it still works if you use the more traditional method syntax - see for yourself by running the example below." + "type": "paragraph", + "value": "Even more interesting is that it still works if you use the more traditional method syntax - see for yourself by running the example below." }, { - "type": "example", - "value": "0.2.4" + "type": "example", + "value": "0.2.4" }, { - "type": "press_enter" + "type": "press_enter" }, { - "type": "paragraph", - "value": "This is a common pattern in Ruby - two different ways to do the same thing where one remains consistent and the other changes the syntax to be more programmer friendly." + "type": "paragraph", + "value": "This is a common pattern in Ruby - two different ways to do the same thing where one remains consistent and the other changes the syntax to be more programmer friendly." }, { - "type": "press_enter" - }] - }] -} + "type": "press_enter" + } + ] + } + ] +} \ No newline at end of file diff --git a/lessons/1.0.json b/lessons/1.0.json index 903a514..6a769e7 100644 --- a/lessons/1.0.json +++ b/lessons/1.0.json @@ -1,77 +1,81 @@ -{ - "subsections": [{ - "name": "Introduction to Strings", - "items": [{ - "type": "paragraph", - "value": "String construction" - }, - { - "type": "paragraph", - "value": "In doing lies the true path, so let us begin by doing. Type 'edit' and then type \"RubyMonk\" in the editor (remember to include the double quotes)." - }, - { - "type": "paragraph", - "value": "After you've edited the file, type 'run' to see what happens." - }, - { - "type": "test_example", - "value": "1.0.0" - }, - { - "type": "paragraph", - "value": "Strings are key to communicating with a user and programming apprentices will encounter and start manipulating strings from the earliest stages of their journey. In this lesson, we will explore some of the tools Ruby provides to create and manipulate strings." - }, - { - "type": "paragraph", - "value": "Literal forms" - }, - { - "type": "paragraph", - "value": "String construction has what is known as a literal form - the interpreter treats anything surrounded with single quotes (') or double quotes(\") as a string. In other words, both 'RubyMonk' and \"RubyMonk\" will create instances of strings." - }, - { - "type": "paragraph", - "value": "It's your turn now; go ahead and create a string that contains the name of the current month. Like, 'April', or 'December'." - }, - { - "type": "test_example", - "value": "1.0.1" - }, - { - "type": "paragraph", - "value": "Action etches deeper into the memory than mere words. Recreate the string in the exercise above using double quotes (\"). You'll see that the tests pass the same way as they did before." - }, - { - "type": "test_example", - "value": "1.0.2" - }, - { - "type": "paragraph", - "value": "The single quoted and double quoted approaches have some differences, which we will look into later. For most purposes they are equivalent." - }, - { - "type": "paragraph", - "value": "All Strings are instances of the Ruby String class which provides a number of methods to manipulate the string. Now that you have successfully mastered creating strings let's take a look at some of the most commonly used methods." - }, - { - "type": "paragraph", - "value": "String Length" - }, - { - "type": "paragraph", - "value": "As a programmer, one of the common things you need to know about a String is its length. Instead of merely telling you how to do this, let me share a secret about the Ruby API with you: Try the obvious! The Ruby core libraries are very intuitive, and with a little practice you can effortlessly convert instructions in plain English to Ruby." - }, - { - "type": "paragraph", - "value": "Test your intuition and try to change the code below to return the length of the string 'RubyMonk'." - }, - { - "type": "test_example", - "value": "1.0.3" - }, - { - "type": "paragraph", - "value": "That shouldn't have taxed your intuition too much! Let's move on to slightly more complex concepts." - }] - }] -} \ No newline at end of file +{ + "subsections": [ + { + "name": "Introduction to Strings", + "items": [ + { + "type": "paragraph", + "value": "String construction" + }, + { + "type": "paragraph", + "value": "In doing lies the true path, so let us begin by doing. Type `edit` and then type `\"RubyMonk\"` in the editor (remember to include the double quotes)." + }, + { + "type": "paragraph", + "value": "After you've edited the file, type `run` to see what happens." + }, + { + "type": "test_example", + "value": "1.0.0" + }, + { + "type": "paragraph", + "value": "Strings are key to communicating with a user and programming apprentices will encounter and start manipulating strings from the earliest stages of their journey. In this lesson, we will explore some of the tools Ruby provides to create and manipulate strings." + }, + { + "type": "paragraph", + "value": "Literal forms" + }, + { + "type": "paragraph", + "value": "String construction has what is known as a literal form - the interpreter treats anything surrounded with single quotes (`'`) or double quotes(`\"`) as a string. In other words, both `'RubyMonk'` and `\"RubyMonk\"` will create instances of strings." + }, + { + "type": "paragraph", + "value": "It's your turn now; go ahead and create a string that contains the name of the current month. Like, `April`, or `December`." + }, + { + "type": "test_example", + "value": "1.0.1" + }, + { + "type": "paragraph", + "value": "Action etches deeper into the memory than mere words. Recreate the string in the exercise above using double quotes (`\"`). You'll see that the tests pass the same way as they did before." + }, + { + "type": "test_example", + "value": "1.0.2" + }, + { + "type": "paragraph", + "value": "The single quoted and double quoted approaches have some differences, which we will look into later. For most purposes they are equivalent." + }, + { + "type": "paragraph", + "value": "All Strings are instances of the Ruby `String` class which provides a number of methods to manipulate the string. Now that you have successfully mastered creating strings let's take a look at some of the most commonly used methods." + }, + { + "type": "paragraph", + "value": "String Length" + }, + { + "type": "paragraph", + "value": "As a programmer, one of the common things you need to know about a `String` is its length. Instead of merely telling you how to do this, let me share a secret about the Ruby API with you: Try the obvious! The Ruby core libraries are very intuitive, and with a little practice you can effortlessly convert instructions in plain English to Ruby." + }, + { + "type": "paragraph", + "value": "Test your intuition and try to change the code below to return the length of the string `RubyMonk`." + }, + { + "type": "test_example", + "value": "1.0.3" + }, + { + "type": "paragraph", + "value": "That shouldn't have taxed your intuition too much! Let's move on to slightly more complex concepts." + } + ] + } + ] +} diff --git a/lessons/toc.json b/lessons/toc.json index f46529c..e3dbf00 100644 --- a/lessons/toc.json +++ b/lessons/toc.json @@ -1,8 +1,8 @@ { - "lessons": [ - "0.0.json", - "0.1.json", - "0.2.json", - "1.0.json" - ] + "lessons": [ + "0.0.json", + "0.1.json", + "0.2.json", + "1.0.json" + ] } diff --git a/lib/justify.rb b/lib/justify.rb deleted file mode 100644 index 615be17..0000000 --- a/lib/justify.rb +++ /dev/null @@ -1,23 +0,0 @@ -# created from the 'justify' gem - http://www.rubydoc.info/gems/justify/1.0.2 -class String - def justify(len = 80) - unless self.length < len - words = self.gsub("\n", " ").scan(/[^ ]+/) - actual_len = 0 - output = "" - words.each do |w| - output += w - actual_len += w.length - if actual_len >= len - output += "\n" - actual_len = 0 - else - output += " " - end - end - return output - else - self - end - end -end diff --git a/lib/sad.rb b/lib/sad.rb deleted file mode 100644 index ef820f8..0000000 --- a/lib/sad.rb +++ /dev/null @@ -1,28 +0,0 @@ -sad = [ - "Do I hear a spec failing?", - "The path to enlightenment is a little long.", - "You shall not pass!", - "This isn't the code I was looking for.", - "Hmm, a valiant attempt. But you must try again!", - "Not quite there yet.", - "No pizza for you. Not yet.", - "Never give up. Ever!", - "Your code did not fail, you just found another method that will not solve this problem.", - "Ten flips, now!", - "If no mistake have you made, yet losing you are ... a different game you should play.", - "No! Try not. Do, or do not. There is no try.", - "No such thing as bad student, only bad teacher. Teacher say, student do.", - "You too much TV.", - "First learn stand, then learn fly. Nature rule, Daniel-san, not mine.", - "Wax on... wax off. Wax on... wax off.", - "Breathe in, breathe out. And no scare fish.", - "Hai! Hurt old man feeling.", - "Oh, Daniel-san! You all wet behind ear!", - "Learn how punch, after you learn how keep dry!", - "Look at this tree, Shifu: I cannot make it blossom when it suits me nor make it bear fruit before its time.", - "There is now a level zero.", - "There are no accidents.", - "Remember the path!", - "When the path you walk always leads back to yourself, you never get anywhere.", - "To make a bad day worse spend it wishing for the impossible." -] diff --git a/lib/sayings.rb b/lib/sayings.rb index 1f875b6..096a714 100644 --- a/lib/sayings.rb +++ b/lib/sayings.rb @@ -1,66 +1,66 @@ module Sayings - def Sayings.get_saying(type) - happy = [ - "More ramen to you!", - "I'm pleased.", - "I see great potential.", - "Thou shalt pass.", - "Good, good!", - "That was quite something.", - "Most excellent.", - "I'm proud of you.", - "Your brothers are proud of you.", - "Your sisters are proud of you.", - "Like a feather, your code floats in the air.", - "Pizza for you! With anchovies I presume?", - "Sweet smell of working code.", - "As you grow older, you will learn there are many ways to do the right thing.", - "Miyagi have hope for you.", - "Good! Now use head for something other than target.", - "Man who catch fly with chopstick accomplish anything.", - "Banzai!", - "Do you want to learn kung fu?", - "I vowed to train you, and you have been trained. You are free to eat.", - "There are no accidents.", - "What goes on inside your head I do not always understand. But what goes on in your heart will never let us down.", - "Beautiful. Elegant. Perfect. You made me proud.", - "The Force will be with you, always." - ] - - sad = [ - "Do I hear a spec failing?", - "The path to enlightenment is a little long.", - "You shall not pass!", - "This isn't the code I was looking for.", - "Hmm, a valiant attempt. But you must try again!", - "Not quite there yet.", - "No pizza for you. Not yet.", - "Never give up. Ever!", - "Your code did not fail, you just found another method that will not solve this problem.", - "Ten flips, now!", - "If no mistake have you made, yet losing you are ... a different game you should play.", - "No! Try not. Do, or do not. There is no try.", - "No such thing as bad student, only bad teacher. Teacher say, student do.", - "You too much TV.", - "First learn stand, then learn fly. Nature rule, Daniel-san, not mine.", - "Wax on... wax off. Wax on... wax off.", - "Breathe in, breathe out. And no scare fish.", - "Hai! Hurt old man feeling.", - "Oh, Daniel-san! You all wet behind ear!", - "Learn how punch, after you learn how keep dry!", - "Look at this tree, Shifu: I cannot make it blossom when it suits me nor make it bear fruit before its time.", - "There is now a level zero.", - "There are no accidents.", - "Remember the path!", - "When the path you walk always leads back to yourself, you never get anywhere.", - "To make a bad day worse spend it wishing for the impossible." - ] + def Sayings.get_saying(type) + happy = [ + "More ramen to you!", + "I'm pleased.", + "I see great potential.", + "Thou shalt pass.", + "Good, good!", + "That was quite something.", + "Most excellent.", + "I'm proud of you.", + "Your brothers are proud of you.", + "Your sisters are proud of you.", + "Like a feather, your code floats in the air.", + "Pizza for you! With anchovies I presume?", + "Sweet smell of working code.", + "As you grow older, you will learn there are many ways to do the right thing.", + "Miyagi have hope for you.", + "Good! Now use head for something other than target.", + "Man who catch fly with chopstick accomplish anything.", + "Banzai!", + "Do you want to learn kung fu?", + "I vowed to train you, and you have been trained. You are free to eat.", + "There are no accidents.", + "What goes on inside your head I do not always understand. But what goes on in your heart will never let us down.", + "Beautiful. Elegant. Perfect. You made me proud.", + "The Force will be with you, always." + ] + + sad = [ + "Do I hear a spec failing?", + "The path to enlightenment is a little long.", + "You shall not pass!", + "This isn't the code I was looking for.", + "Hmm, a valiant attempt. But you must try again!", + "Not quite there yet.", + "No pizza for you. Not yet.", + "Never give up. Ever!", + "Your code did not fail, you just found another method that will not solve this problem.", + "Ten flips, now!", + "If no mistake have you made, yet losing you are ... a different game you should play.", + "No! Try not. Do, or do not. There is no try.", + "No such thing as bad student, only bad teacher. Teacher say, student do.", + "You too much TV.", + "First learn stand, then learn fly. Nature rule, Daniel-san, not mine.", + "Wax on... wax off. Wax on... wax off.", + "Breathe in, breathe out. And no scare fish.", + "Hai! Hurt old man feeling.", + "Oh, Daniel-san! You all wet behind ear!", + "Learn how punch, after you learn how keep dry!", + "Look at this tree, Shifu: I cannot make it blossom when it suits me nor make it bear fruit before its time.", + "There is now a level zero.", + "There are no accidents.", + "Remember the path!", + "When the path you walk always leads back to yourself, you never get anywhere.", + "To make a bad day worse spend it wishing for the impossible." + ] - saying = sad.sample - if type == "happy" - saying = happy.sample - end - saying + saying = sad.sample + if type == "happy" + saying = happy.sample end + saying + end end diff --git a/lib/stringformat.rb b/lib/stringformat.rb new file mode 100644 index 0000000..24a5d07 --- /dev/null +++ b/lib/stringformat.rb @@ -0,0 +1,53 @@ +class String + def format(color: 'white') + require 'tempfile' + require 'base64' + + # Get terminal columns, similar to tput cols + cols = `tput cols`.to_i + em = (cols * 0.60).to_f + + commonmark_content = <<~CMARK + --- + documentclass: standalone + classoption: preview + header-includes: + - | + ```{=latex} + \\usepackage[width=#{em}em]{geometry} + \\usepackage{xcolor} + \\pagecolor{black} + ``` + include-before: + - | + ```{=latex} + \\color{#{color}} + \\Large + ``` + ... + #{self} + CMARK + + tmp_file = Tempfile.new(['stringformat', '.md']) + tmp_file.write(commonmark_content) + tmp_file.close + + pdf_file = tmp_file.path.sub('.md', '.pdf') + + # Run pandoc + system("pandoc --pdf-engine=xelatex -V mainfont=\"IBM Plex Serif Light\" -f commonmark_x #{tmp_file.path} -o #{pdf_file} >/dev/null 2>&1") + + if File.exist?(pdf_file) + pdf_data = File.read(pdf_file) + base64_data = Base64.strict_encode64(pdf_data) + # Clean up + tmp_file.unlink + File.unlink(pdf_file) + # Return iTerm2 inline image sequence + "\e]1337;File=inline=1;preserveAspectRatio=1:#{base64_data}\a" + else + # Fallback if PDF generation fails + self + end + end +end diff --git a/rp b/rp index a54cf4c..1b05f43 100755 --- a/rp +++ b/rp @@ -1,209 +1,235 @@ -#!/usr/bin/ruby +#!/opt/homebrew/opt/ruby@3.2/bin/ruby require 'bundler/setup' require 'colorize' -require 'figlet' +require 'ruby_figlet' require 'tempfile' require 'rspec' require 'rspec/core/formatters/progress_formatter' require 'json' -load 'lib/justify.rb' +load 'lib/stringformat.rb' load 'lib/sayings.rb' def print_header(header) - puts "" - puts "===========================" - font = Figlet::Font.new('fonts/standard.flf') - figlet = Figlet::Typesetter.new(font) - puts figlet[header].magenta - puts "===========================" - puts "" + puts "" + puts "===========================" + figlet = RubyFiglet::Figlet.new(header, 'standard') + puts figlet.to_s.magenta + puts "===========================" + puts "" end def print_welcome() - message = <<-WELCOME - - - Welcome to the Ruby Primer. This is a console version of the Ruby Primer - found at http://rubymonk.com/learning/books/1-ruby-primer, maintained - by C42 Engineering. This is a program written in ruby that runs actual - ruby code snippets in the ruby interpreter on your system. It is intended - to be interactive, fun, and educational. Enjoy! - - WELCOME - puts message.yellow + message = <<-WELCOME +Welcome to the Ruby Primer. This is a console version of the Ruby Primer +previously found at http://rubymonk.com/. This is a program written in `ruby` +that runs actual `ruby` code snippets in the `ruby` interpreter on your system. +It is intended to be interactive, fun, and educational. Enjoy! + WELCOME + puts "\n#{message.format(color:'yellow')}\n\n" end def get_input(question) - print question - STDOUT.flush - STDIN.gets.chomp + print question + STDOUT.flush + STDIN.gets.chomp end def print_paragraph(para, mod=nil) - paratext = para.chomp.justify.cyan - if mod - puts paratext.send(mod) - else - puts paratext - end + paratext = para.chomp + if mod + puts paratext.send(mod) + else puts "" + puts paratext.format + end + puts "" end def print_example(example, test=false, example_id=nil) - print_paragraph("Example Code:", "underline") - print_paragraph(example, "green") - ans = get_input("Type 'edit' to edit the code in vim, or type 'run' to run the code: ") - if ans.downcase == "edit" - puts "Editing code ..." - edited_code = edit_code(example) - print_example(edited_code, test, example_id) - elsif ans.downcase == "run" - puts "Running code ..." - if test - test_code(example, example_id) - else - run_code(example) - end - else - puts "" - puts " ERROR: I'm not sure what you mean by '#{ans}' ... please try again".red - puts "" - print_example(example, test, example_id) + print_paragraph("Example Code:", "underline") + print_paragraph(example, "green") + ans = get_input("Type 'edit' to edit the code in vim, or type 'run' to run the code: ") + if ans.downcase == "edit" + puts "Editing code ..." + edited_code = edit_code(example) + print_example(edited_code, test, example_id) + elsif ans.downcase == "run" + puts "Running code ..." + if test + test_code(example, example_id) + else + run_code(example) end + else + puts "" + puts " ERROR: I'm not sure what you mean by '#{ans}' ... please try again".red + puts "" + print_example(example, test, example_id) + end end def get_example(example_id) - file = File.new("examples/#{example_id}.rb", "r") - contents = file.read - file.close - contents + file = File.new("examples/#{example_id}.rb", "r") + contents = file.read + file.close + contents end def write_code_to_tmpfile(code) - file = Tempfile.new('rubyprimer') - ObjectSpace.undefine_finalizer(file) - file.write(code) - path = file.path - file.close - path + file = Tempfile.new('rubyprimer') + ObjectSpace.undefine_finalizer(file) + file.write(code) + path = file.path + file.close + path end def read_file_and_remove(path) - contents = File.read(path) - File.unlink(path) - contents + contents = File.read(path) + File.unlink(path) + contents end def edit_code(code) - path = write_code_to_tmpfile(code) - command = "/usr/bin/vim #{path}" - pid = spawn(command) - Process.wait(pid) - read_file_and_remove(path) + path = write_code_to_tmpfile(code) + command = "/usr/bin/vim #{path}" + pid = spawn(command) + Process.wait(pid) + read_file_and_remove(path) end def decorate_output(location) - if location == "top" - puts "#### Output: ####".bold.light_green - elsif location == "bottom" - puts "#################".bold.light_green - end + if location == "top" + puts "#### Output: ####".bold.light_green + elsif location == "bottom" + puts "#################".bold.light_green + end end def run_code(code, decorate=true) - decorate_output("top") unless !decorate - command = "#{RbConfig.ruby} -e '#{code}'" - pid = spawn(command) - Process.wait(pid) - puts "" - decorate_output("bottom") unless !decorate + decorate_output("top") unless !decorate + command = "#{RbConfig.ruby} -e '#{code}'" + pid = spawn(command) + Process.wait(pid) + puts "" + decorate_output("bottom") unless !decorate end def test_code(code, example_id) - path = write_code_to_tmpfile(code) - spec = File.read("examples_spec/#{example_id}.rb") - spec.sub! '__TMPFILE__', path - test_path = write_code_to_tmpfile(spec) - decorate_output("top") - - run_code(code, false) - - config = RSpec.configuration - config.color = true - RSpec.clear_examples - status = RSpec::Core::Runner.run([test_path]) - - should_rerun = false - if status == 0 - puts "" - puts "---> SUCCESS: #{Sayings.get_saying('happy')}".bold.light_yellow - puts "" - else - puts "" - puts "---> WHOOPS: #{Sayings.get_saying('sad')}".bold.light_red - puts "" - should_rerun = true - end - decorate_output("bottom") + path = write_code_to_tmpfile(code) + spec = File.read("examples_spec/#{example_id}.rb") + spec.sub! '__TMPFILE__', path + test_path = write_code_to_tmpfile(spec) + decorate_output("top") - File.unlink(path) - File.unlink(test_path) + run_code(code, false) - if should_rerun - print_example(get_example(example_id), true, example_id) - end + config = RSpec.configuration + config.color = true + RSpec.clear_examples + status = RSpec::Core::Runner.run([test_path]) + + should_rerun = false + if status == 0 + puts "" + puts "---> SUCCESS: #{Sayings.get_saying('happy')}".bold.light_yellow + puts "" + else + puts "" + puts "---> WHOOPS: #{Sayings.get_saying('sad')}".bold.light_red + puts "" + should_rerun = true + end + decorate_output("bottom") + + File.unlink(path) + File.unlink(test_path) + + if should_rerun + print_example(get_example(example_id), true, example_id) + end end def press_enter() - get_input("Press ENTER to continue") + get_input("Press ENTER to continue") end def read_json_file(file_name) - file = File.read(file_name) - JSON.parse(file) + file = File.read(file_name) + JSON.parse(file) +end + +def print_toc + toc = read_json_file('lessons/toc.json') + max_name_length = 0 + toc['lessons'].each do |lesson| + lesson_data = read_json_file("lessons/#{lesson}") + lesson_data['subsections'].each do |sub| + max_name_length = [max_name_length, sub['name'].length].max + end + end + width = 10 + max_name_length + puts "" + puts "Table of Contents".center(width).bold.underline + toc['lessons'].each do |lesson| + id = lesson.sub('.json', '') + puts " • #{id}" + lesson_data = read_json_file("lessons/#{lesson}") + lesson_data['subsections'].each do |sub| + puts " ◦ #{sub['name']}" + end + end + puts (" " * width).bold.underline + puts "" + puts " To begin learning with a specific lesson (such as 0.0), run: #{$0} 0.0" + puts "" end def process_toc(lesson_id=nil) - toc = read_json_file('lessons/toc.json') - - toc['lessons'].each do |lesson_file| - if lesson_id - if lesson_file == "#{lesson_id}.json" - print_paragraph("\n\nSkipping to lesson: #{lesson_id}!") - process_lesson("lessons/#{lesson_id}.json") - end - else - process_lesson("lessons/#{lesson_file}") - end + toc = read_json_file('lessons/toc.json') + + toc['lessons'].each do |lesson_file| + if lesson_id + if lesson_file == "#{lesson_id}.json" + print_paragraph("\n\nSkipping to lesson: #{lesson_id}!") + process_lesson("lessons/#{lesson_id}.json") + end + else + process_lesson("lessons/#{lesson_file}") end + end end def process_lesson(lesson_file) - lesson = read_json_file(lesson_file) - lesson['subsections'].each do |subsection| - print_header(subsection['name']) - subsection['items'].each do |item| - case item['type'] - when "press_enter" - press_enter() - when "paragraph" - print_paragraph(item['value']) - when "example" - print_example(get_example(item['value'])) - when "test_example" - print_example(get_example(item['value']), true, item['value']) - end - end + lesson = read_json_file(lesson_file) + lesson['subsections'].each do |subsection| + print_header(subsection['name']) + subsection['items'].each do |item| + case item['type'] + when "press_enter" + press_enter() + when "paragraph" + print_paragraph(item['value']) + when "example" + print_example(get_example(item['value'])) + when "test_example" + print_example(get_example(item['value']), true, item['value']) + end end + end end def main(args) + if args[0] == "toc" + print_toc + else print_welcome() press_enter() lesson_id = args[0] process_toc(lesson_id) + end end ######################################################## main(ARGV)