monadic-chat 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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