monadic-chat 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/CHANGELOG.md +9 -0
  4. data/Gemfile +4 -0
  5. data/Gemfile.lock +172 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +652 -0
  8. data/Rakefile +12 -0
  9. data/apps/chat/chat.json +4 -0
  10. data/apps/chat/chat.md +42 -0
  11. data/apps/chat/chat.rb +79 -0
  12. data/apps/code/code.json +4 -0
  13. data/apps/code/code.md +42 -0
  14. data/apps/code/code.rb +77 -0
  15. data/apps/novel/novel.json +4 -0
  16. data/apps/novel/novel.md +36 -0
  17. data/apps/novel/novel.rb +77 -0
  18. data/apps/translate/translate.json +4 -0
  19. data/apps/translate/translate.md +37 -0
  20. data/apps/translate/translate.rb +81 -0
  21. data/assets/github.css +1036 -0
  22. data/assets/pigments-default.css +69 -0
  23. data/bin/monadic-chat +122 -0
  24. data/doc/img/code-example-time-html.png +0 -0
  25. data/doc/img/code-example-time.png +0 -0
  26. data/doc/img/example-translation.png +0 -0
  27. data/doc/img/how-research-mode-works.svg +1 -0
  28. data/doc/img/input-acess-token.png +0 -0
  29. data/doc/img/langacker-2001.svg +41 -0
  30. data/doc/img/linguistic-html.png +0 -0
  31. data/doc/img/monadic-chat-main-menu.png +0 -0
  32. data/doc/img/monadic-chat.svg +13 -0
  33. data/doc/img/readme-example-beatles-html.png +0 -0
  34. data/doc/img/readme-example-beatles.png +0 -0
  35. data/doc/img/research-mode-template.svg +198 -0
  36. data/doc/img/select-app-menu.png +0 -0
  37. data/doc/img/select-feature-menu.png +0 -0
  38. data/doc/img/state-monad.svg +154 -0
  39. data/doc/img/syntree-sample.png +0 -0
  40. data/lib/monadic_app.rb +115 -0
  41. data/lib/monadic_chat/console.rb +29 -0
  42. data/lib/monadic_chat/formatting.rb +110 -0
  43. data/lib/monadic_chat/helper.rb +72 -0
  44. data/lib/monadic_chat/interaction.rb +41 -0
  45. data/lib/monadic_chat/internals.rb +269 -0
  46. data/lib/monadic_chat/menu.rb +189 -0
  47. data/lib/monadic_chat/open_ai.rb +150 -0
  48. data/lib/monadic_chat/parameters.rb +109 -0
  49. data/lib/monadic_chat/version.rb +5 -0
  50. data/lib/monadic_chat.rb +190 -0
  51. data/monadic_chat.gemspec +54 -0
  52. data/samples/linguistic/linguistic.json +17 -0
  53. data/samples/linguistic/linguistic.md +39 -0
  54. data/samples/linguistic/linguistic.rb +74 -0
  55. metadata +343 -0
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./monadic_chat"
4
+ require_relative "./monadic_chat/console"
5
+ require_relative "./monadic_chat/formatting"
6
+ require_relative "./monadic_chat/interaction"
7
+ require_relative "./monadic_chat/menu"
8
+ require_relative "./monadic_chat/parameters"
9
+ require_relative "./monadic_chat/internals"
10
+
11
+ Thread.abort_on_exception = false
12
+
13
+ class MonadicApp
14
+ include MonadicChat
15
+ attr_reader :template
16
+
17
+ def initialize(params, template, placeholders, prop_accumulated, prop_newdata, update_proc)
18
+ @threads = Thread::Queue.new
19
+ @responses = Thread::Queue.new
20
+ @placeholders = placeholders
21
+ @prop_accumulated = prop_accumulated
22
+ @prop_newdata = prop_newdata
23
+ @completion = nil
24
+ @update_proc = update_proc
25
+ @params_original = params
26
+ @params = @params_original.dup
27
+ @template_original = File.read(template)
28
+ @method = OpenAI.model_to_method @params["model"]
29
+
30
+ case @method
31
+ when "completions"
32
+ @template = @template_original.dup
33
+ when "chat/completions"
34
+ @template = JSON.parse @template_original
35
+ end
36
+ end
37
+
38
+ ##################################################
39
+ # methods for running monadic app
40
+ ##################################################
41
+
42
+ def parse(input = nil)
43
+ loop do
44
+ case input
45
+ when TrueClass
46
+ input = user_input
47
+ next
48
+ when /\A\s*(?:help|menu|commands?|\?|h)\s*\z/i
49
+ return true unless show_menu
50
+ when /\A\s*(?:bye|exit|quit)\s*\z/i
51
+ break
52
+ when /\A\s*(?:reset)\s*\z/i
53
+ reset
54
+ when /\A\s*(?:data|context)\s*\z/i
55
+ show_data
56
+ when /\A\s*(?:html)\s*\z/i
57
+ set_html
58
+ when /\A\s*(?:save)\s*\z/i
59
+ save_data
60
+ when /\A\s*(?:load)\s*\z/i
61
+ load_data
62
+ when /\A\s*(?:clear|clean)\s*\z/i
63
+ clear_screen
64
+ when /\A\s*(?:params?|parameters?|config|configuration)\s*\z/i
65
+ change_parameter
66
+ else
67
+ if input && confirm_query(input)
68
+ begin
69
+ case @method
70
+ when "completions"
71
+ bind_research_mode(input, num_retry: NUM_RETRY)
72
+ when "chat/completions"
73
+ bind_normal_mode(input, num_retry: NUM_RETRY)
74
+ end
75
+ rescue StandardError => e
76
+ input = ask_retrial(input, e.message)
77
+ next
78
+ end
79
+ end
80
+ end
81
+ if input.to_s == ""
82
+ input = false
83
+ clear_screen
84
+ end
85
+ input = user_input
86
+ end
87
+ rescue MonadicError
88
+ false
89
+ end
90
+
91
+ def banner(title, desc, color)
92
+ screen_width = TTY::Screen.width - 2
93
+ width = screen_width < TITLE_WIDTH ? screen_width : TITLE_WIDTH
94
+ title = PASTEL.bold.send(color.to_sym, title.center(width, " "))
95
+ desc = desc.center(width, " ")
96
+ padding = "".center(width, " ")
97
+ banner = TTY::Box.frame "#{padding}\n#{title}\n#{desc}\n#{padding}"
98
+ print "\n", banner.strip, "\n"
99
+ end
100
+
101
+ def run
102
+ banner("MONADIC::CHAT / #{self.class.name}", self.class::DESC, self.class::COLOR)
103
+ show_greet
104
+
105
+ if @placeholders.empty?
106
+ parse(user_input)
107
+ else
108
+ loadfile = PROMPT_SYSTEM.select("\nLoad saved file? (Make sure the file is saved by the same app)", default: 2, show_help: :never) do |menu|
109
+ menu.choice "Yes", "yes"
110
+ menu.choice "No", "no"
111
+ end
112
+ parse(user_input) if loadfile == "yes" && load_data || fulfill_placeholders
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MonadicApp
4
+ ##################################################
5
+ # methods for manipulating terminal screen
6
+ ##################################################
7
+ def count_lines_below
8
+ screen_height = TTY::Screen.height
9
+ vpos = Cursor.pos[:row]
10
+ screen_height - vpos
11
+ end
12
+
13
+ def go_up_and_clear
14
+ print TTY::Cursor.up
15
+ print TTY::Cursor.clear_screen_down
16
+ print TTY::Cursor.up
17
+ end
18
+
19
+ def clear_screen
20
+ print "\e[2J\e[f"
21
+ end
22
+
23
+ def ask_clear
24
+ PROMPT_SYSTEM.readline(PASTEL.red("Press Enter to clear screen"))
25
+ print TTY::Cursor.up
26
+ print TTY::Cursor.clear_screen_down
27
+ clear_screen
28
+ end
29
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MonadicApp
4
+ ##################################################
5
+ # methods for formatting and presenting
6
+ ##################################################
7
+ def format_data
8
+ contextual = []
9
+ accumulated = []
10
+
11
+ objectify.each do |key, val|
12
+ next if %w[prompt response].include? key
13
+
14
+ if (@method == "completions" && key == @prop_accumulated) ||
15
+ (@method == "chat/completions" && key == "messages")
16
+ val = val.map do |v|
17
+ case @method
18
+ when "completions"
19
+ if v.instance_of?(String)
20
+ v.sub(/\s+###\s*$/m, "")
21
+ else
22
+ v.map { |role, text| "#{role.strip.capitalize}: #{text.sub(/\s+###\s*$/m, "")}" }
23
+ end
24
+ when "chat/completions"
25
+ "#{v["role"].capitalize}: #{v["content"]}"
26
+ end
27
+ end
28
+ accumulated << val.join("\n\n")
29
+ else
30
+ contextual << "- **#{key.split("_").map(&:capitalize).join(" ")}**: #{val.to_s.strip}"
31
+ end
32
+ end
33
+
34
+ h1 = "# Monadic :: Chat / #{self.class.name}"
35
+ contextual.map!(&:strip).unshift "## Contextual Data\n" unless contextual.empty?
36
+ accum_label = @prop_accumulated.split("_").map(&:capitalize).join(" ")
37
+ accumulated.map!(&:strip).unshift "## #{accum_label}\n" unless accumulated.empty?
38
+ "#{h1}\n\n#{contextual.join("\n")}\n\n#{accumulated.join("\n")}"
39
+ end
40
+
41
+ def show_data
42
+ print PROMPT_SYSTEM.prefix
43
+
44
+ wait
45
+
46
+ res = format_data
47
+ print "\n#{TTY::Markdown.parse(res, indent: 0)}"
48
+ end
49
+
50
+ def set_html
51
+ print PROMPT_SYSTEM.prefix
52
+
53
+ wait
54
+
55
+ print "HTML is ready\n"
56
+ show_html
57
+ end
58
+
59
+ def add_to_html(text, filepath)
60
+ text = text.gsub(/(?<![\\>\s])(?!\n[\n<])\n/m) { "<br/>\n" }
61
+ text = text.gsub(/~~~(.+?)~~~/m) do
62
+ m = Regexp.last_match
63
+ "~~~#{m[1].gsub("<br/>\n") { "\n" }}~~~"
64
+ end
65
+ text = text.gsub(/`(.+?)`/) do
66
+ m = Regexp.last_match
67
+ "`#{m[1].gsub("<br/>\n") { "\n" }}`"
68
+ end
69
+
70
+ `touch #{filepath}` unless File.exist?(filepath)
71
+ File.open(filepath, "w") do |f|
72
+ html = <<~HTML
73
+ <!doctype html>
74
+ <html lang="en">
75
+ <head>
76
+ <meta charset="utf-8">
77
+ <meta name="viewport" content="width=device-width, initial-scale=1">
78
+ <style type="text/css">
79
+ #{GITHUB_STYLE}
80
+ </style>
81
+ <title>Monadic Chat</title>
82
+ </head>
83
+ <body>
84
+ #{Kramdown::Document.new(text, syntax_highlighter: :rouge, syntax_highlighter_ops: {}).to_html}
85
+ </body>
86
+ <script src="https://code.jquery.com/jquery-3.6.3.min.js"></script>
87
+ <script src="https://code.jquery.com/ui/1.13.2/jquery-ui.min.js"></script>
88
+ <script>
89
+ $(window).on("load", function() {
90
+ $("html, body").animate({ scrollTop: $(document).height() }, 500);
91
+ });
92
+ </script>
93
+ </html>
94
+ HTML
95
+ f.write html
96
+ end
97
+ Launchy.open(filepath)
98
+ end
99
+
100
+ def show_html
101
+ res = format_data.sub(%r{::(.+?)/(.+?)\b}) do
102
+ " <span class='monadic_gray'>::</span> <span class='monadic_app'>#{Regexp.last_match(1)}</span> <span class='monadic_gray'>/</span> #{Regexp.last_match(2)}"
103
+ end
104
+ res = res.gsub("```") { "~~~" }
105
+ .gsub(/^(system):/i) { "<span class='monadic_system'> #{Regexp.last_match(1)} </span><br />" }
106
+ .gsub(/^(user):/i) { "<span class='monadic_user'> #{Regexp.last_match(1)} </span><br />" }
107
+ .gsub(/^(assistant|gpt):/i) { "<span class='monadic_chat'> #{Regexp.last_match(1)} </span><br />" }
108
+ add_to_html(res, TEMP_HTML)
109
+ end
110
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Cursor
4
+ class << self
5
+ def pos
6
+ res = +""
7
+ $stdin.raw do |stdin|
8
+ $stdout << "\e[6n"
9
+ $stdout.flush
10
+ while (c = stdin.getc) != "R"
11
+ res << c if c
12
+ end
13
+ end
14
+ m = res.match(/(?<row>\d+);(?<column>\d+)/)
15
+ { row: Integer(m[:row]), column: Integer(m[:column]) }
16
+ end
17
+ end
18
+ end
19
+
20
+ module TTY
21
+ class PromptX < Prompt
22
+ attr_reader :prefix
23
+
24
+ def initialize(active_color:, prefix:, history: true)
25
+ @interrupt = lambda do
26
+ print TTY::Cursor.clear_screen_down
27
+ print "\e[2J\e[f"
28
+ res = TTY::Prompt.new.yes?("Quit the app?")
29
+ exit if res
30
+ end
31
+
32
+ super(active_color: active_color, prefix: prefix, interrupt: @interrupt)
33
+ @history = history
34
+ @prefix = prefix
35
+ end
36
+
37
+ def readline(text = "")
38
+ puts @prefix
39
+ begin
40
+ Readline.readline(text, @history)
41
+ rescue Interrupt
42
+ @interrupt.call
43
+ end
44
+ end
45
+ end
46
+
47
+ module Markdown
48
+ # Converts a Kramdown::Document tree to a terminal friendly output
49
+ class Converter < ::Kramdown::Converter::Base
50
+ def convert_p(ell, opts)
51
+ indent = SPACE * @current_indent
52
+ result = []
53
+
54
+ result << indent unless %i[blockquote li].include?(opts[:parent].type)
55
+
56
+ opts[:indent] = @current_indent
57
+ opts[:indent] = 0 if opts[:parent].type == :blockquote
58
+
59
+ content = inner(ell, opts)
60
+
61
+ symbols = %q{[-!$%^&*()_+|~=`{}\[\]:";'<>?,.\/]}
62
+ # result << content.join.gsub(/(?<!#{symbols})\n(?!#{symbols})/m) { " " }.gsub(/ +/) { " " }
63
+ result << content.join.gsub(/(?<!#{symbols})\n(?!#{symbols})/m) { "" }
64
+ result << NEWLINE unless result.last.to_s.end_with?(NEWLINE)
65
+ result
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ class MonadicError < StandardError
72
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MonadicApp
4
+ ##################################################
5
+ # methods for user interaction
6
+ ##################################################
7
+
8
+ def user_input(text = "")
9
+ if count_lines_below < 1
10
+ ask_clear
11
+ user_input
12
+ else
13
+ res = PROMPT_USER.readline(text)
14
+ print TTY::Cursor.clear_line_after
15
+ res == "" ? nil : res
16
+ end
17
+ end
18
+
19
+ def show_greet
20
+ current_mode = case @method
21
+ when "completions"
22
+ PASTEL.red("Research")
23
+ when "chat/completions"
24
+ PASTEL.green("Normal")
25
+ end
26
+ greet_md = <<~GREET
27
+ - You are currently in **#{current_mode}** mode
28
+ - Type **help** or **menu** to see available commands
29
+ GREET
30
+ print PROMPT_SYSTEM.prefix
31
+ print "\n#{TTY::Markdown.parse(greet_md, indent: 0).strip}\n"
32
+ end
33
+
34
+ def confirm_query(input)
35
+ if input.size < MIN_LENGTH
36
+ PROMPT_SYSTEM.yes?("Would you like to proceed with this (very short) prompt?")
37
+ else
38
+ true
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,269 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MonadicApp
4
+ ##################################################
5
+ # methods for preparation and updating
6
+ ##################################################
7
+
8
+ def fulfill_placeholders
9
+ input = nil
10
+ replacements = []
11
+ mode = :replace
12
+
13
+ @placeholders.each do |key, val|
14
+ if key == "mode"
15
+ mode = val
16
+ next
17
+ end
18
+
19
+ input = if mode == :replace
20
+ val
21
+ else
22
+ PROMPT_SYSTEM.readline("#{val}: ")
23
+ end
24
+
25
+ unless input
26
+ replacements.clear
27
+ break
28
+ end
29
+ replacements << [key, input]
30
+ end
31
+ if replacements.empty?
32
+ false
33
+ else
34
+ replacements.each do |key, value|
35
+ case @method
36
+ when "completions"
37
+ @template.gsub!(key, value)
38
+ when "chat/completions"
39
+ @template["messages"][0]["content"].gsub!(key, value)
40
+ end
41
+ end
42
+ true
43
+ end
44
+ end
45
+
46
+ def wait
47
+ return self if @threads.empty?
48
+
49
+ print TTY::Cursor.save
50
+ message = PASTEL.red "Processing contextual data #{SPINNER} "
51
+ print message
52
+
53
+ TIMEOUT_SEC.times do |i|
54
+ raise MonadicError, "Error: something went wrong" if i + 1 == TIMEOUT_SEC
55
+
56
+ break if @threads.empty?
57
+
58
+ sleep 1
59
+ end
60
+ print TTY::Cursor.restore
61
+ print TTY::Cursor.clear_char(message.size)
62
+
63
+ self
64
+ end
65
+
66
+ def objectify
67
+ case @method
68
+ when "completions"
69
+ m = /\n\n```json\s*(\{.+\})\s*```\n\n/m.match(@template)
70
+ json = m[1].gsub(/(?!\\\\\\)\\\\"/) { '\\\"' }
71
+ JSON.parse(json)
72
+ when "chat/completions"
73
+ @template
74
+ end
75
+ end
76
+
77
+ def prepare_params(input)
78
+ params = @params.dup
79
+ case @method
80
+ when "completions"
81
+ template = @template.dup.sub("{{PROMPT}}", input).sub("{{MAX_TOKENS}}", (@params["max_tokens"] / 2).to_s)
82
+ params["prompt"] = template
83
+ when "chat/completions"
84
+ @template["messages"] << { "role" => "user", "content" => input }
85
+ params["messages"] = @template["messages"]
86
+ end
87
+ params
88
+ end
89
+
90
+ def update_template(res)
91
+ case @method
92
+ when "completions"
93
+ updated = @update_proc.call(res)
94
+ json = updated.to_json.strip
95
+ @template.sub!(/\n\n```json.+```\n\n/m, "\n\n```json\n#{json}\n```\n\n")
96
+ when "chat/completions"
97
+ @template["messages"] << { "role" => "assistant", "content" => res }
98
+ @template["messages"] = @update_proc.call(@template["messages"])
99
+ end
100
+ end
101
+
102
+ ##################################################
103
+ # functions for binding data
104
+ ##################################################
105
+
106
+ def bind_normal_mode(input, num_retry: 0)
107
+ print PROMPT_ASSISTANT.prefix, "\n"
108
+ print TTY::Cursor.save
109
+
110
+ wait
111
+
112
+ params = prepare_params(input)
113
+ print TTY::Cursor.save
114
+
115
+ escaping = +""
116
+ last_chunk = +""
117
+ response = +""
118
+ spinning = false
119
+ res = @completion.run(params, num_retry: num_retry) do |chunk|
120
+ if escaping
121
+ chunk = escaping + chunk
122
+ escaping = ""
123
+ end
124
+
125
+ if /(?:\\\z)/ =~ chunk
126
+ escaping += chunk
127
+ next
128
+ else
129
+ chunk = chunk.gsub('\\n', "\n")
130
+ response << chunk
131
+ end
132
+
133
+ if count_lines_below > 1
134
+ print PASTEL.magenta(last_chunk)
135
+ elsif !spinning
136
+ print PASTEL.red SPINNER
137
+ spinning = true
138
+ end
139
+
140
+ last_chunk = chunk
141
+ end
142
+
143
+ print TTY::Cursor.restore
144
+ print TTY::Cursor.clear_screen_down
145
+
146
+ text = response.gsub(/(?<![\\>\s])(?!\n[\n<])\n/m) { "{{NEWLINE}}\n" }
147
+ text = text.gsub(/```(.+?)```/m) do
148
+ m = Regexp.last_match
149
+ "```#{m[1].gsub("{{NEWLINE}}\n") { "\n" }}```"
150
+ end
151
+ text = text.gsub(/`(.+?)`/) do
152
+ m = Regexp.last_match
153
+ "`#{m[1].gsub("{{NEWLINE}}\n") { "\n" }}`"
154
+ end
155
+
156
+ text = text.gsub(/(?!\\\\)\\/) { "" }
157
+ print TTY::Markdown.parse(text).gsub("{{NEWLINE}}") { "\n" }.strip
158
+ print "\n"
159
+
160
+ update_template(res)
161
+ end
162
+
163
+ def bind_research_mode(input, num_retry: 0)
164
+ print PROMPT_ASSISTANT.prefix, "\n"
165
+
166
+ wait
167
+
168
+ params = prepare_params(input)
169
+ print TTY::Cursor.save
170
+
171
+ @threads << true
172
+ Thread.new do
173
+ response_all_shown = false
174
+ key_start = /"#{@prop_newdata}":\s*"/
175
+ key_finish = /\s+###\s*"/m
176
+ started = false
177
+ escaping = +""
178
+ last_chunk = +""
179
+ finished = false
180
+ response = +""
181
+ spinning = false
182
+ res = @completion.run(params, num_retry: num_retry) do |chunk|
183
+ if finished && !response_all_shown
184
+ response_all_shown = true
185
+ @responses << response.sub(/\s+###\s*".*/m, "")
186
+ if spinning
187
+ TTY::Cursor.backword(" ▹▹▹▹▹ ".size)
188
+ TTY::Cursor.clear_char(" ▹▹▹▹▹ ".size)
189
+ end
190
+ end
191
+
192
+ unless finished
193
+ if escaping
194
+ chunk = escaping + chunk
195
+ escaping = ""
196
+ end
197
+
198
+ if /(?:\\\z)/ =~ chunk
199
+ escaping += chunk
200
+ next
201
+ else
202
+ chunk = chunk.gsub('\\n', "\n")
203
+ response << chunk
204
+ end
205
+
206
+ if started && !finished
207
+ if key_finish =~ response
208
+ finished = true
209
+ else
210
+ if count_lines_below > 1
211
+ print PASTEL.magenta(last_chunk)
212
+ elsif !spinning
213
+ print PASTEL.red SPINNER
214
+ spinning = true
215
+ end
216
+ last_chunk = chunk
217
+ end
218
+ elsif !started && !finished && key_start =~ response
219
+ started = true
220
+ response = +""
221
+ end
222
+ end
223
+ end
224
+
225
+ unless response_all_shown
226
+ if spinning
227
+ TTY::Cursor.backword(SPINNER.size)
228
+ TTY::Cursor.clear_char(SPINNER.size)
229
+ end
230
+ @responses << response.sub(/\s+###\s*".*/m, "")
231
+ end
232
+
233
+ update_template(res)
234
+ @threads.clear
235
+ rescue StandardError => e
236
+ @threads.clear
237
+ @responses << <<~ERROR
238
+ Error: something went wrong in a thread"
239
+ #{e.message}
240
+ #{e.backtrace}
241
+ ERROR
242
+ end
243
+
244
+ loop do
245
+ if @responses.empty?
246
+ sleep 1
247
+ else
248
+ print TTY::Cursor.restore
249
+ print TTY::Cursor.clear_screen_down
250
+ text = @responses.pop
251
+
252
+ text = text.gsub(/(?<![\\>\s])(?!\n[\n<])\n/m) { "{{NEWLINE}}\n" }
253
+ text = text.gsub(/```(.+?)```/m) do
254
+ m = Regexp.last_match
255
+ "```#{m[1].gsub("{{NEWLINE}}\n") { "\n" }}```"
256
+ end
257
+ text = text.gsub(/`(.+?)`/) do
258
+ m = Regexp.last_match
259
+ "`#{m[1].gsub("{{NEWLINE}}\n") { "\n" }}`"
260
+ end
261
+
262
+ text = text.gsub(/(?!\\\\)\\/) { "" }
263
+ print TTY::Markdown.parse(text).gsub("{{NEWLINE}}") { "\n" }.strip
264
+ print "\n"
265
+ break
266
+ end
267
+ end
268
+ end
269
+ end