cosensee 0.6.0

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 (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