From 74c6cb902bec4d0c428dba3cac3dec2f40152981 Mon Sep 17 00:00:00 2001 From: Schneems Date: Mon, 6 May 2024 10:03:13 -0500 Subject: [PATCH] Introduce print.text and print.erb See the README for more details. --- CHANGELOG.md | 1 + README.md | 99 +++++++++++- lib/rundoc/code_command.rb | 2 + lib/rundoc/code_command/print/erb.rb | 50 ++++++ lib/rundoc/code_command/print/text.rb | 33 ++++ lib/rundoc/code_section.rb | 16 +- test/integration/print_test.rb | 195 ++++++++++++++++++++++++ test/rundoc/code_commands/print_test.rb | 94 ++++++++++++ test/rundoc/parser_test.rb | 2 - 9 files changed, 486 insertions(+), 6 deletions(-) create mode 100644 lib/rundoc/code_command/print/erb.rb create mode 100644 lib/rundoc/code_command/print/text.rb create mode 100644 test/integration/print_test.rb create mode 100644 test/rundoc/code_commands/print_test.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 09a4492..206a091 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## HEAD +- Add: `print.text` and `print.erb` see README for usage details - Add: Output documents include more frequent warnings that the document was autogenerated and should not be modified - Breaking: Remove repl_runner support. - Fix `file.write` do not prepend "In file x ..." when the command is hidden i.e. `:::-> file.write` diff --git a/README.md b/README.md index c7cd3dd..7ef5411 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,9 @@ This will generate a project folder with your project in it, and a markdown READ - Execute Bash Commands - [$](#shell-commands) - [fail.$](#shell-commands) +- Printing + - [print.text](#print) + - [print.erb](#print) - Chain commands - [pipe](#pipe) - [|](#pipe) @@ -181,7 +184,7 @@ RunDOC only cares about things that come after a `:::` section. If you have a "r You can mix non-command code and commands, as long as the things that aren't rendering come first. This can be used to "fake" a command, for example: ``` -$ rails new myapp # Not a command since it's missing the ":::>>"" +$ rails new myapp # Not a command since it's missing the ":::>>" :::-> $ rails new myapp --skip-test --skip-yarn --skip-sprockets :::>> | $ head -n 5 ``` @@ -197,13 +200,12 @@ $ rails new myapp # Not a command since it's missing the ":::>>"" create config.ru ``` -It looks like the command was run without any flags, but in reality `rails new myapp --skip-test --skip-yarn --skip-sprockets | head -n 5` was executed. +In this example it looks like the command was run without any flags, but in reality `rails new myapp --skip-test --skip-yarn --skip-sprockets | head -n 5` was executed. Though it's more explicit to use a `print.text` block, see [#print.text](#print) for more info. ## Rendering Cheat Sheet An arrow `>` is shorthand for "render this" and a dash `-` is shorthand for skip this section. The two positions are **command** first and **result** second. - - `:::>-` (YES command output, not result output) - `:::>>` (YES command output, YES result output) - `:::--` (not command output, not result output) @@ -250,6 +252,64 @@ These custom commands are kept to a minimum, and for the most part behave as you Running shell commands like this can be very powerful, you'll likely want more control of how you manipulate files in your project. To do this you can use the `file.` namespace: +## Print + +Current commands: + +- `print.text` +- `print.erb` + +Behaves slightly differently than other commands. The "command" portion of the control character i.e. `:::>` controls whether the contents will be rendered inside the block or before the block (versus usually this is used to control if the command such as `$ cd` is shown). + +- `:::>>` Print inside the code block +- `:::->` Print BEFORE the code block, if multiple calls are made, they will be displayed in order. +- `:::--` Nothing will be rendered, can be used to pass data to another rundoc command via the pipe operator. +- `:::>-` Same behavior as `:::--`. + +This functionality is present to allow body text to be generated (versus only allowing generated text in code blocks). + +Use the `print.text` keyword followed by what you want to print: + + ``` + :::-> print.text + I will render BEFORE the code block, use :::>> to render in it. + + It was the best of times, it was the worst of times, it was the age of wisdom, it was the age of foolishness, + it was the epoch of belief, it was the epoch ... + ``` + +Specifying `:::->` with `print.text` will render text without a code block (or before the code block if there are other rundoc commands). If you want to render text with a code block you can do it via `:::>>`. + +To dynamically change the contents of the thing you're printing you can use `print.erb`: + + ``` + :::-> print.erb + I will render BEFORE the code block, use :::>> to render in it. + + What a week! + Captain it's only <%= Time.now.strftime("%A") %>! + ``` + +This will evaluate the context of ERB and write it to the file. Like `print.text` use `:::->` to write the contents without a code block (or before the code block if there are other rundoc commands). If you want to render text with a code block you can do it via `:::>>`. + +ERB commands share a default context. That means you can set a value in one `print.erb` section and view it from another. If you want to isolate your erb blocks you can provide a custom name via the `binding:` keyword: + + ``` + :::>> print.erb(binding: "mc_hammer") + I will render IN a code block, use `:::->` to render before. + + <%= @stop = true %> + + :::>> print.erb(binding: "different") + <% if @stop %> + Hammer time + <% else %> + Can't touch this + <% end %> + ``` + +In this example setting `@stop` in one `print.erb` will have no effect on the other. + ## File Commands Current Commands: @@ -504,6 +564,39 @@ Sometimes sensitive info like usernames, email addresses, or passwords may be in This command `filter_sensitive` can be called multiple times with different values. Since the config is in Ruby you could iterate over an array of sensitive data +## Writing a new command + +Rundoc does not have a stable internal command interface. You can define your own commands, but unless it is committed in this repo, it may break on a minor version change. + +To add a new command it needs to be parsed and called. Examples of commands being implemented are seen in `lib/rundoc/code_command`. + +A new command needs to be registered: + +``` +Rundoc.register_code_command(:lol, Rundoc::CodeCommand::Lol) +``` + +They should inherit from Rundoc::CodeCommand: + +``` +class Rundoc::CodeCommand::Lol < Rundoc::CodeCommand + def initialize(line) + end +end +``` + +The initialize method is called with input from the document. The command is rendered (`:::>-`) by the output of the `def call` method. The contents produced by the command (`:::->`) are rendered by the `def to_md` method. + +The syntax for commands is ruby-ish but it is a custom grammar implemented in `lib/peg_parser.rb` for more info on manipulating the grammar see this tutorial on how I added keword-like/hash-like syntax https://github.com/schneems/implement_ruby_hash_syntax_with_parslet_example. + +Command initialize methods natively support: + +- Barewords as a single string input +- Keyword arguments +- A combination of the two + +Anything that is passed to the command via "stdin" is available via a method `self.contents`. The interplay between the input and `self.contents` is not strongly defined. + ## Copyright All content Copyright Richard Schneeman © 2020 diff --git a/lib/rundoc/code_command.rb b/lib/rundoc/code_command.rb index 4b1ce03..1dab385 100644 --- a/lib/rundoc/code_command.rb +++ b/lib/rundoc/code_command.rb @@ -59,3 +59,5 @@ def to_md(env = {}) require "rundoc/code_command/raw" require "rundoc/code_command/background" require "rundoc/code_command/website" +require "rundoc/code_command/print/text" +require "rundoc/code_command/print/erb" diff --git a/lib/rundoc/code_command/print/erb.rb b/lib/rundoc/code_command/print/erb.rb new file mode 100644 index 0000000..6fbc446 --- /dev/null +++ b/lib/rundoc/code_command/print/erb.rb @@ -0,0 +1,50 @@ + +require "erb" + +class EmptyBinding + def self.create + self.new.empty_binding + end + + def empty_binding + binding + end +end + +RUNDOC_ERB_BINDINGS = Hash.new {|h, k| h[k] = EmptyBinding.create } + +class Rundoc::CodeCommand + class PrintERB < Rundoc::CodeCommand + + def initialize(line = nil, binding: "default") + @line = line + @binding = RUNDOC_ERB_BINDINGS[binding] + end + + def to_md(env) + if render_before? + env[:before] << render + end + + "" + end + + def render + @render ||= ERB.new([@line, contents].compact.join("\n")).result(@binding) + end + + def call(erb={}) + if render_before? + "" + else + render + end + end + + def render_before? + !render_command? && render_result? + end + end +end + +Rundoc.register_code_command(:"print.erb", Rundoc::CodeCommand::PrintERB) diff --git a/lib/rundoc/code_command/print/text.rb b/lib/rundoc/code_command/print/text.rb new file mode 100644 index 0000000..08dd424 --- /dev/null +++ b/lib/rundoc/code_command/print/text.rb @@ -0,0 +1,33 @@ +class Rundoc::CodeCommand + class PrintText < Rundoc::CodeCommand + def initialize(line) + @line = line + end + + def to_md(env) + if render_before? + env[:before] << [@line, contents].compact.join("\n") + end + + "" + end + + def hidden? + !render_result? + end + + def call(env = {}) + if render_before? + "" + else + [@line, contents].compact.join("\n") + end + end + + def render_before? + !render_command? && render_result? + end + end +end + +Rundoc.register_code_command(:"print.text", Rundoc::CodeCommand::PrintText) diff --git a/lib/rundoc/code_section.rb b/lib/rundoc/code_section.rb index 0877934..29cb3c7 100644 --- a/lib/rundoc/code_section.rb +++ b/lib/rundoc/code_section.rb @@ -73,7 +73,21 @@ def render return "" if hidden? - array = [env[:before], env[:fence_start], result, env[:fence_end], env[:after]] + array = [env[:before]] + + result.flatten! + result.compact! + result.map! {|s| s.respond_to?(:rstrip) ? s.rstrip : s } + result.reject!(&:empty?) + result.map!(&:to_s) + + if !result.empty? + array << env[:fence_start] + array << result + array << env[:fence_end] + end + array << env[:after] + array.flatten! array.compact! array.map! {|s| s.respond_to?(:rstrip) ? s.rstrip : s } diff --git a/test/integration/print_test.rb b/test/integration/print_test.rb new file mode 100644 index 0000000..86235ff --- /dev/null +++ b/test/integration/print_test.rb @@ -0,0 +1,195 @@ +require "test_helper" + +class ParserTest < Minitest::Test + def test_erb_shared_binding_persists_values + key = SecureRandom.hex + contents = <<~RUBY + ``` + :::-> print.erb + one <% @variable = "#{key}" %> + :::-> print.erb + <%= @variable %> + ``` + RUBY + + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + env = {} + env[:before] = [] + expected = <<~EOF + one + #{key} + EOF + parsed = Rundoc::Parser.new(contents) + actual = parsed.to_md.gsub(Rundoc::CodeSection::AUTOGEN_WARNING, "") + assert_equal expected, actual + end + end + + key = SecureRandom.hex + contents = <<~RUBY + ``` + :::-> print.erb(binding: "one") + one <% @variable = "#{key}" %> + :::-> print.erb(binding: "different") + <%= @variable %> + ``` + RUBY + + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + env = {} + env[:before] = [] + expected = <<~EOF + one + EOF + parsed = Rundoc::Parser.new(contents) + actual = parsed.to_md.gsub(Rundoc::CodeSection::AUTOGEN_WARNING, "") + assert_equal expected, actual + end + end + end + + def test_erb_in_block + contents = <<~RUBY + ``` + :::>> print.erb + Hello + there + ``` + RUBY + + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + env = {} + env[:before] = [] + expected = <<~EOF + ``` + Hello + there + ``` + EOF + parsed = Rundoc::Parser.new(contents) + actual = parsed.to_md.gsub(Rundoc::CodeSection::AUTOGEN_WARNING, "") + assert_equal expected, actual + end + end + end + + + def test_erb_with_default_binding + contents = <<~RUBY + ``` + :::-> print.erb + Hello + there + ``` + RUBY + + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + env = {} + env[:before] = [] + expected = <<~EOF + Hello + there + EOF + parsed = Rundoc::Parser.new(contents) + actual = parsed.to_md.gsub(Rundoc::CodeSection::AUTOGEN_WARNING, "") + assert_equal expected, actual + end + end + end + + def test_erb_with_explicit_binding + contents = <<~RUBY + ``` + :::-> print.erb(binding: "yolo") + Hello + there + ``` + RUBY + + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + env = {} + env[:before] = [] + expected = <<~EOF + Hello + there + EOF + parsed = Rundoc::Parser.new(contents) + actual = parsed.to_md.gsub(Rundoc::CodeSection::AUTOGEN_WARNING, "") + assert_equal expected, actual + end + end + end + + def test_print_text_stdin_contents + contents = <<~RUBY + ``` + :::-> print.text + Hello + there + ``` + RUBY + + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + env = {} + env[:before] = [] + expected = <<~EOF + Hello + there + EOF + parsed = Rundoc::Parser.new(contents) + actual = parsed.to_md.gsub(Rundoc::CodeSection::AUTOGEN_WARNING, "") + assert_equal expected, actual + end + end + end + + def test_print_before + contents = <<~RUBY + ``` + :::-> print.text Hello there + ``` + RUBY + + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + env = {} + env[:before] = [] + expected = <<~EOF + Hello there + EOF + parsed = Rundoc::Parser.new(contents) + actual = parsed.to_md.gsub(Rundoc::CodeSection::AUTOGEN_WARNING, "") + assert_equal expected, actual + end + end + end + + def test_print_in_block + contents = <<~RUBY + ``` + :::>> print.text Hello there + ``` + RUBY + + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + env = {} + env[:before] = [] + expected = <<~EOF + ``` + Hello there + ``` + EOF + parsed = Rundoc::Parser.new(contents) + actual = parsed.to_md.gsub(Rundoc::CodeSection::AUTOGEN_WARNING, "") + assert_equal expected, actual + end + end + end +end diff --git a/test/rundoc/code_commands/print_test.rb b/test/rundoc/code_commands/print_test.rb new file mode 100644 index 0000000..a04938d --- /dev/null +++ b/test/rundoc/code_commands/print_test.rb @@ -0,0 +1,94 @@ +require "test_helper" + +class PrintTest < Minitest::Test + def test_plain_text_before_block + env = {} + env[:before] = [] + + input = %Q{$ rails new myapp # Not a command since it's missing the ":::>>"} + cmd = Rundoc::CodeCommand::PrintText.new(input) + cmd.render_command = false + cmd.render_result = true + + assert_equal "", cmd.to_md(env) + assert_equal "", cmd.call + assert_equal ["$ rails new myapp # Not a command since it's missing the \":::>>\""], env[:before] + end + + def test_plain_text_in_block + env = {} + env[:before] = [] + + input = %Q{$ rails new myapp # Not a command since it's missing the ":::>>"} + cmd = Rundoc::CodeCommand::PrintText.new(input) + cmd.render_command = true + cmd.render_result = true + + assert_equal "", cmd.to_md(env) + assert_equal input, cmd.call + + assert_equal [], env[:before] + end + + def test_erb_before_block + env = {} + env[:before] = [] + + input = %Q{$ rails new <%= 'myapp' %> # Not a command since it's missing the ":::>>"} + cmd = Rundoc::CodeCommand::PrintERB.new(input) + cmd.render_command = false + cmd.render_result = true + + assert_equal "", cmd.to_md(env) + assert_equal "", cmd.call + assert_equal ["$ rails new myapp # Not a command since it's missing the \":::>>\""], + env[:before] + end + + def test_erb_in_block + env = {} + env[:before] = [] + + cmd = Rundoc::CodeCommand::PrintERB.new() + cmd.contents = %Q{<%= "foo" %>} + cmd.render_command = true + cmd.render_result = true + + assert_equal "", cmd.to_md(env) + assert_equal "foo", cmd.call + assert_equal [], env[:before] + end + + def test_binding_is_preserved + env = {} + env[:before] = [] + cmd = Rundoc::CodeCommand::PrintERB.new() + cmd.contents = %Q{<%= @foo = SecureRandom.hex(16) %>} + cmd.render_command = true + cmd.render_result = true + + assert_equal "", cmd.to_md(env) + assert_equal [], env[:before] + expected = cmd.call + + assert !expected.empty? + + cmd = Rundoc::CodeCommand::PrintERB.new() + cmd.contents = %Q{<%= @foo %>} + cmd.render_command = true + cmd.render_result = true + + assert_equal "", cmd.to_md(env) + assert_equal expected, cmd.call + assert_equal [], env[:before] + + cmd = Rundoc::CodeCommand::PrintERB.new(binding: "different") + cmd.contents = %Q{<%= @foo %>} + cmd.render_command = true + cmd.render_result = true + + assert_equal "", cmd.to_md(env) + assert_equal "", cmd.call + assert_equal [], env[:before] + end +end diff --git a/test/rundoc/parser_test.rb b/test/rundoc/parser_test.rb index fc24b91..a784d3f 100644 --- a/test/rundoc/parser_test.rb +++ b/test/rundoc/parser_test.rb @@ -1,8 +1,6 @@ require "test_helper" class ParserTest < Minitest::Test - def setup - end def test_parse_bash contents = <<~RUBY