cosensee 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +56 -0
  4. data/.rubocop_todo.yml +7 -0
  5. data/README.md +40 -0
  6. data/Rakefile +10 -0
  7. data/lib/cosensee/blank_bracket.rb +8 -0
  8. data/lib/cosensee/bracket_parser.rb +82 -0
  9. data/lib/cosensee/bracket_serializer.rb +26 -0
  10. data/lib/cosensee/code.rb +18 -0
  11. data/lib/cosensee/codeblock.rb +18 -0
  12. data/lib/cosensee/command_line.rb +18 -0
  13. data/lib/cosensee/decorate_bracket.rb +16 -0
  14. data/lib/cosensee/double_bracket.rb +20 -0
  15. data/lib/cosensee/empty_bracket.rb +8 -0
  16. data/lib/cosensee/error.rb +5 -0
  17. data/lib/cosensee/external_link_bracket.rb +8 -0
  18. data/lib/cosensee/formula_bracket.rb +8 -0
  19. data/lib/cosensee/hash_tag.rb +19 -0
  20. data/lib/cosensee/html_builder.rb +59 -0
  21. data/lib/cosensee/html_encodable.rb +12 -0
  22. data/lib/cosensee/icon_bracket.rb +8 -0
  23. data/lib/cosensee/image_bracket.rb +8 -0
  24. data/lib/cosensee/indent.rb +25 -0
  25. data/lib/cosensee/internal_link_bracket.rb +8 -0
  26. data/lib/cosensee/line.rb +47 -0
  27. data/lib/cosensee/line_parser.rb +253 -0
  28. data/lib/cosensee/link.rb +18 -0
  29. data/lib/cosensee/link_encodable.rb +26 -0
  30. data/lib/cosensee/page.rb +68 -0
  31. data/lib/cosensee/page_store.rb +63 -0
  32. data/lib/cosensee/parsed_bracket.rb +42 -0
  33. data/lib/cosensee/parsed_line.rb +76 -0
  34. data/lib/cosensee/project.rb +46 -0
  35. data/lib/cosensee/quote.rb +22 -0
  36. data/lib/cosensee/tailwind_renderer/blank_bracket.rb +11 -0
  37. data/lib/cosensee/tailwind_renderer/code.rb +14 -0
  38. data/lib/cosensee/tailwind_renderer/codeblock_builder.rb +37 -0
  39. data/lib/cosensee/tailwind_renderer/command_line.rb +17 -0
  40. data/lib/cosensee/tailwind_renderer/decorate_bracket.rb +25 -0
  41. data/lib/cosensee/tailwind_renderer/double_bracket.rb +13 -0
  42. data/lib/cosensee/tailwind_renderer/empty_bracket.rb +11 -0
  43. data/lib/cosensee/tailwind_renderer/external_link_bracket.rb +15 -0
  44. data/lib/cosensee/tailwind_renderer/formula_bracket.rb +13 -0
  45. data/lib/cosensee/tailwind_renderer/hash_tag.rb +17 -0
  46. data/lib/cosensee/tailwind_renderer/icon_bracket.rb +14 -0
  47. data/lib/cosensee/tailwind_renderer/image_bracket.rb +15 -0
  48. data/lib/cosensee/tailwind_renderer/internal_link_bracket.rb +15 -0
  49. data/lib/cosensee/tailwind_renderer/link.rb +14 -0
  50. data/lib/cosensee/tailwind_renderer/page.rb +31 -0
  51. data/lib/cosensee/tailwind_renderer/parsed_line.rb +30 -0
  52. data/lib/cosensee/tailwind_renderer/quote.rb +14 -0
  53. data/lib/cosensee/tailwind_renderer/text_bracket.rb +12 -0
  54. data/lib/cosensee/tailwind_renderer.rb +36 -0
  55. data/lib/cosensee/text_bracket.rb +10 -0
  56. data/lib/cosensee/user.rb +37 -0
  57. data/lib/cosensee/version.rb +5 -0
  58. data/lib/cosensee.rb +54 -0
  59. data/sig/cosensee.rbs +4 -0
  60. data/templates/index.html.erb +39 -0
  61. data/templates/page.html.erb +59 -0
  62. metadata +130 -0
@@ -0,0 +1,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Cosensee
6
+ # parse a line
7
+ class LineParser
8
+ INDENT_PATTERN = /\A([\t ]*)(.*)\z/
9
+ QUOTE_PATTERN = /\A(>)(.*)\z/
10
+ CODEBLOCK_PATTERN = /\A(code:)(.+)\z/
11
+ COMMANDLINE_PATTERN = /\A([$%]) (.+)\z/
12
+
13
+ def self.parse(line)
14
+ new.parse(line)
15
+ end
16
+
17
+ def initialize
18
+ @bracket_parser = Cosensee::BracketParser.new
19
+ end
20
+
21
+ def parse(line)
22
+ parsed_line = ParsedLine.new(rest: line)
23
+ parsed_line
24
+ .then { parse_indent(it) }
25
+ .then { parse_whole_line(it) }
26
+ .then { parse_code(it) }
27
+ .then { parse_double_bracket(it) }
28
+ .then { parse_bracket(it) }
29
+ .then { parse_url(it) }
30
+ .then { parse_hashtag(it) }
31
+ .then { done_parsing(it) }
32
+ end
33
+
34
+ def parse_indent(line)
35
+ matched = line.match(INDENT_PATTERN)
36
+ ParsedLine.new(indent: Cosensee::Indent.new(matched[1], matched[1]),
37
+ rest: matched[2])
38
+ end
39
+
40
+ def parse_whole_line(line)
41
+ # parse quote
42
+ matched = line.match(QUOTE_PATTERN)
43
+ if matched
44
+ line.update(rest: matched[2],
45
+ line_content: Cosensee::Quote.new(content: nil, raw: matched[0], mark: matched[1]))
46
+ return line
47
+ end
48
+
49
+ # parse codeblock
50
+ matched = line.match(CODEBLOCK_PATTERN)
51
+ if matched
52
+ return line.update(rest: nil,
53
+ line_content: Cosensee::Codeblock.new(content: matched[2], raw: matched[0]),
54
+ parsed: true)
55
+ end
56
+
57
+ # parse command line
58
+ matched = line.match(COMMANDLINE_PATTERN)
59
+ if matched
60
+ return line.update(rest: nil,
61
+ line_content: Cosensee::CommandLine.new(content: matched[2],
62
+ prompt: matched[1],
63
+ raw: matched[0]),
64
+ parsed: true)
65
+ end
66
+
67
+ line
68
+ end
69
+
70
+ def parse_code(line)
71
+ return line if line.parsed?
72
+
73
+ parsed = []
74
+ strs = line.split_rest_by('`')
75
+ loop do
76
+ str = strs.shift
77
+ unless str
78
+ return line.update(rest: nil,
79
+ content: parsed)
80
+ end
81
+
82
+ parsed << str
83
+
84
+ str = strs.shift
85
+ unless str
86
+ line.update(rest: nil,
87
+ content: parsed)
88
+ return line
89
+ end
90
+
91
+ if strs.empty?
92
+ parsed.last.concat("`#{str}")
93
+ return line.update(rest: nil,
94
+ content: parsed)
95
+ else
96
+ parsed << Code.new(str, "`#{str}`")
97
+ end
98
+ end
99
+ end
100
+
101
+ def parse_hashtag(line)
102
+ return line if line.parsed?
103
+
104
+ parsed = []
105
+
106
+ line.content.each do |elem|
107
+ if elem.is_a?(String)
108
+ loop do
109
+ matched = elem.match(/(^|\s)#(\S+)/)
110
+ if matched
111
+ parsed << "#{matched.pre_match}#{matched[1]}"
112
+ parsed << Cosensee::HashTag.new(content: matched[2], raw: "##{matched[2]}")
113
+ elem = matched.post_match
114
+ else
115
+ parsed << elem
116
+ break # loop
117
+ end
118
+ end
119
+ else
120
+ parsed << elem
121
+ end
122
+ end
123
+
124
+ line.update(content: clean_elements(parsed))
125
+ end
126
+
127
+ def parse_url(line)
128
+ return line if line.parsed?
129
+
130
+ parsed = []
131
+
132
+ line.content.each do |elem|
133
+ if elem.is_a?(String)
134
+ loop do
135
+ matched = elem.match(%r{(^|\s)(https?://[^\s]+)})
136
+ if matched
137
+ parsed << "#{matched.pre_match}#{matched[1]}"
138
+ parsed << Cosensee::Link.new(matched[2], matched[2])
139
+ elem = matched.post_match
140
+ else
141
+ parsed << elem
142
+ break # loop
143
+ end
144
+ end
145
+ else
146
+ parsed << elem
147
+ end
148
+ end
149
+
150
+ line.update(content: clean_elements(parsed))
151
+ end
152
+
153
+ def parse_double_bracket(line)
154
+ return line if line.parsed?
155
+
156
+ parsed = []
157
+
158
+ line.content.each do |elem|
159
+ if elem.is_a?(String)
160
+ loop do
161
+ matched = elem.match(/\[\[(.+?)\]\]/)
162
+ if matched
163
+ parsed << matched.pre_match
164
+ parsed << Cosensee::DoubleBracket.new(content: [matched[1]], raw: matched[0])
165
+ elem = matched.post_match
166
+ else
167
+ parsed << elem
168
+ break # loop
169
+ end
170
+ end
171
+ else
172
+ parsed << elem
173
+ end
174
+ end
175
+
176
+ line.update(content: clean_elements(parsed))
177
+ end
178
+
179
+ def parse_bracket(line)
180
+ return line if line.parsed?
181
+
182
+ parsed = []
183
+ stack = nil
184
+ target_char = '[' # or "]"
185
+
186
+ line.content.each do |elem|
187
+ case elem
188
+ when Cosensee::Code, Cosensee::DoubleBracket
189
+ if target_char == '['
190
+ parsed << elem
191
+ else
192
+ stack << elem
193
+ end
194
+ else # elem is String
195
+ loop do
196
+ n = elem.index(target_char)
197
+ if n
198
+ if target_char == '['
199
+ parsed << elem[0, n]
200
+ target_char = ']'
201
+ stack = []
202
+ else
203
+ stack << elem[0, n]
204
+ target_char = '['
205
+ parsed << @bracket_parser.parse(stack)
206
+ stack = nil
207
+ end
208
+ elem = elem[(n + 1)..]
209
+ else
210
+ if target_char == '['
211
+ parsed << elem
212
+ else
213
+ stack << elem
214
+ end
215
+ break
216
+ end
217
+ end
218
+ end
219
+ end
220
+
221
+ if stack
222
+ # parsed += ['['] + stack
223
+ parsed << '['
224
+ parsed.concat(stack)
225
+ end
226
+
227
+ line.update(content: clean_elements(parsed))
228
+ end
229
+
230
+ def done_parsing(line)
231
+ # If the line_content is Cosensee::Quote, move content into it.
232
+ if line.line_content.is_a?(Cosensee::Quote)
233
+ new_quote = line.line_content.replace_content(line.content)
234
+ line.update(line_content: new_quote, content: [])
235
+ end
236
+ line.update(parsed: true)
237
+ end
238
+
239
+ def clean_elements(elements)
240
+ fixed = []
241
+ elements.each do |elem|
242
+ last_elem = fixed.last
243
+ if last_elem.is_a?(String) && elem.is_a?(String)
244
+ fixed[-1] = "#{last_elem}#{elem}"
245
+ else
246
+ fixed << elem
247
+ end
248
+ end
249
+
250
+ fixed.filter { |elem| !elem.nil? && elem != '' }
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Cosensee
6
+ # link node
7
+ Link = Data.define(:content, :raw) do
8
+ alias_method :to_s, :raw
9
+
10
+ def to_obj
11
+ content
12
+ end
13
+
14
+ def to_json(*)
15
+ to_obj.to_json(*)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module Cosensee
6
+ # parser of Bracket
7
+ module LinkEncodable
8
+ UNESCAPED_REGEX = /[A-Za-z0-9!"\$&'\(\)\-\~@+;:*<>,._]/
9
+
10
+ def make_link(anchor)
11
+ "#{encode_link(anchor)}.html"
12
+ end
13
+
14
+ def encode_link(str)
15
+ str.chars.map do |char|
16
+ if char.match?(UNESCAPED_REGEX)
17
+ char
18
+ elsif char == ' '
19
+ '_'
20
+ else
21
+ URI.encode_www_form_component(char)
22
+ end
23
+ end.join
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ # require 'uri'
5
+
6
+ module Cosensee
7
+ # for Page
8
+ Page = Data.define(:id, :title, :created, :updated, :views, :lines) do
9
+ def self.from_array(args_list)
10
+ args_list.map { |args| new(**args) }
11
+ end
12
+
13
+ def self.from_hash(obj)
14
+ new(**obj)
15
+ end
16
+
17
+ def initialize(id:, title:, created:, updated:, views:, lines:)
18
+ @parsed_lines = Cosensee::Line.from_array(lines.drop(1)).map(&:parsed)
19
+ @linking_page_titles = @parsed_lines.map(&:internal_links).flatten
20
+
21
+ super(
22
+ id:,
23
+ title:,
24
+ created: Time.at(created),
25
+ updated: Time.at(updated),
26
+ views:,
27
+ lines:
28
+ )
29
+ end
30
+
31
+ attr_accessor :parsed_lines, :linking_page_titles
32
+
33
+ def body_lines
34
+ lines.drop(1)
35
+ end
36
+
37
+ def some_images?
38
+ lines.any?(&:some_image?)
39
+ end
40
+
41
+ def first_image
42
+ lines.find(&:some_image?)&.first_image
43
+ end
44
+
45
+ def link_path
46
+ # body = URI.encode_www_form_component(title.gsub(/ /, '_'))
47
+ body = title.gsub(/ /, '_').gsub('/', '%2F')
48
+
49
+ "#{body}.html"
50
+ end
51
+
52
+ def to_html
53
+ Cosensee::TailwindRenderer.new(content: self).render
54
+ end
55
+
56
+ def to_obj
57
+ { id:, title:, created: created.to_i, updated: updated.to_i, views:, lines: }
58
+ end
59
+
60
+ def to_json(*)
61
+ to_obj.to_json(*)
62
+ end
63
+
64
+ def inspect
65
+ "#<data Cosensee::Page id: #{id}, title: #{title}>"
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ # require 'uri'
5
+
6
+ module Cosensee
7
+ # To search for pages
8
+ class PageStore
9
+ def initialize(project:)
10
+ @project = project
11
+ @pages = project.pages
12
+ @pages_by_title = nil
13
+ @linking_pages = nil
14
+ @linked_pages = nil
15
+ end
16
+
17
+ def pages_by_title
18
+ @pages_by_title ||= create_title_index(@pages)
19
+ end
20
+
21
+ def linking_pages
22
+ setup_link_indexes unless @linking_pages
23
+
24
+ @linking_pages
25
+ end
26
+
27
+ def linked_pages
28
+ setup_link_indexes unless @linked_pages
29
+
30
+ @linked_pages
31
+ end
32
+
33
+ def title_exist?(title)
34
+ !!pages_by_title[title]
35
+ end
36
+
37
+ def orphan_page_titles
38
+ linked_pages.keys.reject { |title| title_exist?(title) }
39
+ end
40
+
41
+ def create_title_index(pages)
42
+ pages.each_with_object({}) do |page, hash|
43
+ hash[page.title] = page
44
+ end
45
+ end
46
+
47
+ def find_link_pages_by_title(title)
48
+ pages = linking_pages[title] + linked_pages[title]
49
+ pages.sort_by(&:updated)
50
+ end
51
+
52
+ def setup_link_indexes
53
+ @linking_pages = Hash.new { |h, k| h[k] = [] }
54
+ @linked_pages = Hash.new { |h, k| h[k] = [] }
55
+ @project.pages.each do |page|
56
+ page.linking_page_titles.each do |linking_title|
57
+ @linking_pages[page.title] << pages_by_title[linking_title] if pages_by_title[linking_title]
58
+ @linked_pages[linking_title] << page
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cosensee
4
+ # parse a line
5
+ class ParsedBracket
6
+ def initialize(content: [])
7
+ @content = content
8
+ @parsed = false
9
+ end
10
+
11
+ attr_accessor :content
12
+
13
+ def parsed?
14
+ @parsed
15
+ end
16
+
17
+ def update(**attributes)
18
+ attributes.each do |key, value|
19
+ # Check if the key is a valid accessor
20
+ raise ArgumentError, "Attribute #{key} is not allowed to be updated." unless self.class.method_defined?("#{key}=")
21
+
22
+ public_send("#{key}=", value)
23
+ end
24
+
25
+ self
26
+ end
27
+
28
+ def single_text?
29
+ content.size == 1 && content.first.is_a?(String)
30
+ end
31
+
32
+ def first_content
33
+ content.first
34
+ end
35
+
36
+ def ==(other)
37
+ other.is_a?(Cosensee::ParsedBracket) &&
38
+ other.content == content &&
39
+ other.parsed? == parsed?
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cosensee
4
+ # parse a line
5
+ class ParsedLine
6
+ def initialize(indent: nil, line_content: nil, content: [], rest: nil, parsed: false)
7
+ @indent = indent
8
+ @line_content = line_content
9
+ @content = content
10
+ @rest = rest
11
+ @parsed = parsed
12
+ end
13
+
14
+ attr_accessor :indent, :line_content, :content, :rest, :parsed
15
+
16
+ def codeblock?
17
+ line_content.is_a?(Cosensee::Codeblock)
18
+ end
19
+
20
+ def parsed?
21
+ @parsed
22
+ end
23
+
24
+ def rest?
25
+ !!rest
26
+ end
27
+
28
+ def line_content?
29
+ !!line_content
30
+ end
31
+
32
+ def indent_level
33
+ indent.level
34
+ end
35
+
36
+ def match(pattern)
37
+ rest.match(pattern)
38
+ end
39
+
40
+ def split_rest_by(str)
41
+ rest.split(str, -1)
42
+ end
43
+
44
+ def update(**attributes)
45
+ attributes.each do |key, value|
46
+ # Check if the key is a valid accessor
47
+ raise ArgumentError, "Attribute #{key} is not allowed to be updated." unless self.class.method_defined?("#{key}=")
48
+
49
+ public_send("#{key}=", value)
50
+ end
51
+
52
+ self
53
+ end
54
+
55
+ def internal_links
56
+ content.select { |c| c.is_a?(Cosensee::InternalLinkBracket) || c.is_a?(Cosensee::HashTag) }.map(&:anchor)
57
+ end
58
+
59
+ def raw
60
+ if line_content?
61
+ "#{indent.raw}#{line_content.raw}"
62
+ else
63
+ "#{indent.raw}#{content.map(&:to_s).join}"
64
+ end
65
+ end
66
+
67
+ def ==(other)
68
+ other.is_a?(Cosensee::ParsedLine) &&
69
+ other.indent == indent &&
70
+ other.line_content == line_content &&
71
+ other.content == content &&
72
+ other.rest == rest &&
73
+ other.parsed? == parsed?
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Cosensee
6
+ # for Project
7
+ class Project
8
+ def self.parse(source)
9
+ json = JSON.parse(source, symbolize_names: true)
10
+ name, display_name, exported, users, pages = json.values_at(:name, :displayName, :exported, :users, :pages)
11
+ Cosensee::Project.new(name:, display_name:, exported:, users:, pages:, source:)
12
+ end
13
+
14
+ def self.parse_file(filename)
15
+ src = File.read(filename)
16
+ parse(src)
17
+ end
18
+
19
+ def initialize(name:, exported:, users:, pages:, source:, **kwargs)
20
+ @name = name
21
+ @display_name = if kwargs.keys.size == 1 && kwargs.key?(:display_name)
22
+ kwargs[:display_name]
23
+ elsif kwargs.keys.size == 1 && kwargs.key?(:displayName)
24
+ kwargs[:displayName]
25
+ else
26
+ raise Cosensee::Error, 'Cosensee::User.new need an argument :display_name or :displayName'
27
+ end
28
+ @users = Cosensee::User.from_array(users)
29
+ @pages = Cosensee::Page.from_array(pages)
30
+ @exported = Time.at(exported)
31
+ @source = source
32
+
33
+ @page_store = PageStore.new(project: self)
34
+ end
35
+
36
+ attr_reader :name, :display_name, :users, :pages, :exported, :source, :page_store
37
+
38
+ def to_obj
39
+ { name:, displayName: display_name, exported: exported.to_i, users:, pages: }
40
+ end
41
+
42
+ def to_json(*)
43
+ to_obj.to_json(*)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Cosensee
6
+ # for quote
7
+ Quote = Data.define(:content, :raw, :mark) do
8
+ alias_method :to_s, :raw
9
+
10
+ def replace_content(new_content)
11
+ Quote.new(content: new_content, raw:, mark:)
12
+ end
13
+
14
+ def to_obj
15
+ content
16
+ end
17
+
18
+ def to_json(*)
19
+ to_obj.to_json(*)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cosensee
4
+ class TailwindRenderer
5
+ BlankBracket = Data.define(:content) do
6
+ def render
7
+ %(<span>#{content.blank}</span>)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cosensee
4
+ class TailwindRenderer
5
+ Code = Data.define(:content) do
6
+ include HtmlEncodable
7
+
8
+ def render
9
+ code = content.content
10
+ %(<code class="bg-gray-100 text-red-500 px-1 py-0.5 rounded">#{escape_html(code)}</code>)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cosensee
4
+ class TailwindRenderer
5
+ # connect parsed lines to a code block
6
+ class CodeblockBuilder
7
+ include HtmlEncodable
8
+
9
+ def initialize(parsed_line)
10
+ @first_line = parsed_line
11
+ @base_indent_level = parsed_line.indent_level
12
+ @lines = []
13
+ end
14
+
15
+ attr_reader :first_line, :lines, :base_indent_level
16
+
17
+ def append(parsed_line)
18
+ @lines << parsed_line
19
+ end
20
+
21
+ def continued_line?(parsed_line)
22
+ parsed_line.indent_level >= base_indent_level
23
+ end
24
+
25
+ def render
26
+ indent_level = base_indent_level * 2
27
+ title = first_line.line_content.content
28
+ <<~HTML_BLOCK
29
+ <div class="relative pl-[#{indent_level}rem]">
30
+ <div class="bg-orange-300 text-gray-900 px-4 py-1 rounded-t-lg font-mono text-sm">#{title}</div>
31
+ <div class="px-4 bg-gray-300 text-gray-900 rounded-b-lg shadow-md"><pre class="overflow-x-auto"><code class="block font-mono text-sm leading-relaxed">#{escape_html(lines.map(&:raw).join("\n"))}</code></pre></div>
32
+ </div>
33
+ HTML_BLOCK
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Cosensee
6
+ class TailwindRenderer
7
+ CommandLine = Data.define(:content) do
8
+ include HtmlEncodable
9
+
10
+ def render
11
+ code = content.content
12
+ prompt = content.prompt
13
+ %(<code class="bg-gray-100 text-gray-800 px-4"><span class="text-red-400">#{prompt}</span>#{escape_html(code)}</code>)
14
+ end
15
+ end
16
+ end
17
+ end