mkd 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +93 -0
- data/Rakefile +12 -0
- data/exe/mkd +7 -0
- data/lib/mkd/renderer.rb +369 -0
- data/lib/mkd/version.rb +5 -0
- data/lib/mkd.rb +14 -0
- data/screenshot.png +0 -0
- data/sig/mkd.rbs +4 -0
- metadata +96 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: cdff350d72636cbe26d157b998b0fcdd3c492085ac4a8d41a349b4ee7ee963c5
|
|
4
|
+
data.tar.gz: 8dc3585e64bb312649648d6d45028fb6d92051214f2e69144b0da9f086285be0
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d33f2419baa2873e7585b1cf398f09026e1613c1d945a6b453983fd2d47f45fabccb12006c3dad1dfe906aba7c363fcb973c50e9c64c64e4439cd96ba8275141
|
|
7
|
+
data.tar.gz: 5c138f78eab2f4621147716b3bb7c6aab31e42e9c96809f6ff2733128de83a4a001026a33e96a35228632b6142846e18424ba6f9674e6f9f3d441e94d0c2e830
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Akira Matsuda
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# mkd
|
|
2
|
+
|
|
3
|
+
A terminal Markdown viewer. Renders Markdown with ANSI escape codes and the Kitty graphics/text sizing protocol.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
gem install mkd
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or add to your Gemfile:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bundle add mkd
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
mkd README.md
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or pipe from stdin:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
cat README.md | mkd
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
From Ruby:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
require 'mkd'
|
|
33
|
+
print Mkd.render(markdown_string)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
### Inline formatting
|
|
39
|
+
|
|
40
|
+
- **Bold**, *italic*, ***bold italic***, ~~strikethrough~~, `inline code`
|
|
41
|
+
- [Hyperlinks](https://example.com) via OSC 8 terminal sequences
|
|
42
|
+
- :emoji: shortcodes (GitHub-style, powered by gemoji)
|
|
43
|
+
|
|
44
|
+
### Headings
|
|
45
|
+
|
|
46
|
+
- H1 and H2 use scaled text via the text sizing protocol (OSC 66) when the terminal supports it, falling back to bold text otherwise
|
|
47
|
+
- Terminal support is auto-detected at runtime by probing cursor position
|
|
48
|
+
- H3 through H6 use bold, underline, and dim ANSI styles
|
|
49
|
+
|
|
50
|
+
### Code blocks
|
|
51
|
+
|
|
52
|
+
Syntax-highlighted fenced code blocks via Rouge:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
puts "hello, world"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Lists
|
|
59
|
+
|
|
60
|
+
- Unordered lists with bullet characters
|
|
61
|
+
- Ordered (numbered) lists
|
|
62
|
+
- Nested lists with increasing indentation
|
|
63
|
+
- GFM task lists:
|
|
64
|
+
- [ ] unchecked
|
|
65
|
+
- [x] checked
|
|
66
|
+
|
|
67
|
+
### Tables
|
|
68
|
+
|
|
69
|
+
Rendered with box-drawing characters:
|
|
70
|
+
|
|
71
|
+
| Feature | Status |
|
|
72
|
+
|----------|--------|
|
|
73
|
+
| Tables | :white_check_mark: |
|
|
74
|
+
| Alignment| :white_check_mark: |
|
|
75
|
+
|
|
76
|
+
### Other
|
|
77
|
+
|
|
78
|
+
- Block quotes with vertical bar
|
|
79
|
+
- Horizontal rules
|
|
80
|
+
- Images displayed inline via the Kitty graphics protocol (PNG, SVG)
|
|
81
|
+
- Clickable image links
|
|
82
|
+
|
|
83
|
+
## Screenshot
|
|
84
|
+

|
|
85
|
+
|
|
86
|
+
## Requirements
|
|
87
|
+
|
|
88
|
+
- A terminal with ANSI escape code support
|
|
89
|
+
- [Kitty](https://sw.kovidgoyal.net/kitty/) (or compatible terminal, like [Echoes](https://github.com/amatsuda/echoes)) for image display and scaled headings (auto-detected; works without these in degraded mode)
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/exe/mkd
ADDED
data/lib/mkd/renderer.rb
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'io/console'
|
|
4
|
+
require 'open-uri'
|
|
5
|
+
require 'redcarpet'
|
|
6
|
+
require 'rouge'
|
|
7
|
+
|
|
8
|
+
module Mkd
|
|
9
|
+
class Renderer < Redcarpet::Render::Base
|
|
10
|
+
def initialize
|
|
11
|
+
super
|
|
12
|
+
@ordered_counter = 0
|
|
13
|
+
@text_sizing = detect_text_sizing
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Inline callbacks
|
|
17
|
+
|
|
18
|
+
def normal_text(text)
|
|
19
|
+
text
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def emphasis(text)
|
|
23
|
+
"\e[3m#{text}\e[23m"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def double_emphasis(text)
|
|
27
|
+
"\e[1m#{text}\e[22m"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def triple_emphasis(text)
|
|
31
|
+
"\e[1m\e[3m#{text}\e[23m\e[22m"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def strikethrough(text)
|
|
35
|
+
"\e[9m#{text}\e[29m"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def codespan(code)
|
|
39
|
+
"\e[7m#{code}\e[27m"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def linebreak
|
|
43
|
+
"\n"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def link(link, _title, content)
|
|
47
|
+
if content.include?("\e_G") && @last_image_dimensions
|
|
48
|
+
apc = content.chomp.sub("\e_Ga=T,", "\e_Ga=T,C=1,")
|
|
49
|
+
cols, rows = image_cells(*@last_image_dimensions)
|
|
50
|
+
lines = Array.new(rows) { ' ' * cols }.join("\n")
|
|
51
|
+
"#{apc}\e]8;;#{link}\a#{lines}\e]8;;\a\n"
|
|
52
|
+
elsif content.include?("\e_G")
|
|
53
|
+
"#{content.chomp}\e]8;;#{link}\a\u2197\e]8;;\a\n"
|
|
54
|
+
else
|
|
55
|
+
"\e]8;;#{link}\a#{content}\e]8;;\a"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def autolink(link, _link_type)
|
|
60
|
+
"\e]8;;#{link}\a\e[4m#{link}\e[24m\e]8;;\a"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def image(link, _title, alt_text)
|
|
64
|
+
@last_image_dimensions = nil
|
|
65
|
+
if link.match?(%r{\Ahttps?://})
|
|
66
|
+
data = URI.parse(link).read
|
|
67
|
+
data = rsvg_convert(data) if link.end_with?('.svg')
|
|
68
|
+
@last_image_dimensions = png_dimensions(data)
|
|
69
|
+
"#{kitty_image_data(data)}\n"
|
|
70
|
+
else
|
|
71
|
+
path = File.expand_path(link)
|
|
72
|
+
if File.file?(path)
|
|
73
|
+
if path.end_with?('.svg')
|
|
74
|
+
data = rsvg_convert(File.binread(path))
|
|
75
|
+
@last_image_dimensions = png_dimensions(data)
|
|
76
|
+
"#{kitty_image_data(data)}\n"
|
|
77
|
+
else
|
|
78
|
+
@last_image_dimensions = png_dimensions(File.binread(path, 24))
|
|
79
|
+
"#{kitty_image(path)}\n"
|
|
80
|
+
end
|
|
81
|
+
else
|
|
82
|
+
"\e[2m[image: #{alt_text}]\e[22m"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
rescue
|
|
86
|
+
@last_image_dimensions = nil
|
|
87
|
+
"\e[2m[image: #{alt_text}]\e[22m"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Block callbacks
|
|
91
|
+
|
|
92
|
+
def paragraph(text)
|
|
93
|
+
"#{text}\n\n"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def header(text, header_level)
|
|
97
|
+
case header_level
|
|
98
|
+
when 1
|
|
99
|
+
before, after, clean = extract_heading_parts(text)
|
|
100
|
+
sized = @text_sizing ? kitty_sized(clean, scale: 3) : clean
|
|
101
|
+
"#{before}\e[1m#{sized}\n\n\n\e[22m#{after}"
|
|
102
|
+
when 2
|
|
103
|
+
before, after, clean = extract_heading_parts(text)
|
|
104
|
+
sized = @text_sizing ? kitty_sized(clean, scale: 2) : clean
|
|
105
|
+
"#{before}\e[1m#{sized}\n\n\e[22m#{after}"
|
|
106
|
+
when 3
|
|
107
|
+
"\e[1m#{text}\e[22m\n\n"
|
|
108
|
+
when 4
|
|
109
|
+
"\e[1m\e[4m#{text}\e[24m\e[22m\n\n"
|
|
110
|
+
when 5
|
|
111
|
+
"\e[1m\e[2m#{text}\e[22m\e[22m\n\n"
|
|
112
|
+
when 6
|
|
113
|
+
"\e[2m#{text}\e[22m\n\n"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def block_code(code, language)
|
|
118
|
+
lexer = Rouge::Lexer.find(language) || Rouge::Lexers::PlainText.new
|
|
119
|
+
formatter = Rouge::Formatters::Terminal256.new
|
|
120
|
+
highlighted = formatter.format(lexer.lex(code))
|
|
121
|
+
indented = highlighted.lines.map { |line| " #{line}" }.join
|
|
122
|
+
"#{indented}\n"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def block_quote(content)
|
|
126
|
+
content.lines.map { |line| "\e[2m\u2502\e[22m #{line}" }.join
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def table(header, body)
|
|
130
|
+
rows = parse_table_rows(header + body)
|
|
131
|
+
num_cols = rows.first&.size || 0
|
|
132
|
+
alignments = @table_alignments&.first(num_cols) || []
|
|
133
|
+
@table_alignments = nil
|
|
134
|
+
|
|
135
|
+
col_widths = Array.new(num_cols, 0)
|
|
136
|
+
rows.each do |row|
|
|
137
|
+
row.each_with_index do |cell, i|
|
|
138
|
+
w = visible_length(cell)
|
|
139
|
+
col_widths[i] = w if w > col_widths[i]
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
header_rows = parse_table_rows(header)
|
|
144
|
+
body_rows = parse_table_rows(body)
|
|
145
|
+
|
|
146
|
+
result = +''
|
|
147
|
+
result << table_border(col_widths, '┌', '┬', '┐')
|
|
148
|
+
header_rows.each { |row| result << table_data_row(row, col_widths, alignments, bold: true) }
|
|
149
|
+
result << table_border(col_widths, '├', '┼', '┤')
|
|
150
|
+
body_rows.each { |row| result << table_data_row(row, col_widths, alignments, bold: false) }
|
|
151
|
+
result << table_border(col_widths, '└', '┴', '┘')
|
|
152
|
+
"#{result}\n"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def table_row(content)
|
|
156
|
+
"#{content}\x01"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def table_cell(content, alignment, _header)
|
|
160
|
+
@table_alignments ||= []
|
|
161
|
+
@table_alignments << alignment
|
|
162
|
+
"#{content}\x00"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def list(content, _list_type)
|
|
166
|
+
@ordered_counter = 0
|
|
167
|
+
content.lines.map { |l| " #{l}" }.join + "\n"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def list_item(text, list_type)
|
|
171
|
+
first_line, rest = text.split("\n", 2)
|
|
172
|
+
first_line = first_line.strip
|
|
173
|
+
|
|
174
|
+
prefix = if list_type == :ordered
|
|
175
|
+
@ordered_counter += 1
|
|
176
|
+
"#{@ordered_counter}. "
|
|
177
|
+
elsif first_line.start_with?('[ ] ')
|
|
178
|
+
first_line = first_line.delete_prefix('[ ] ')
|
|
179
|
+
"\u2610 "
|
|
180
|
+
elsif first_line.match?(/\A\[[xX]\] /)
|
|
181
|
+
first_line = first_line.sub(/\A\[[xX]\] /, '')
|
|
182
|
+
"\u2611 "
|
|
183
|
+
else
|
|
184
|
+
"\u2022 "
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
result = "#{prefix}#{first_line}\n"
|
|
188
|
+
if rest
|
|
189
|
+
rest = rest.gsub(/\n+\z/, "\n")
|
|
190
|
+
result << rest unless rest.strip.empty?
|
|
191
|
+
end
|
|
192
|
+
result
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def hrule
|
|
196
|
+
"#{'─' * 40}\n\n"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def doc_footer
|
|
200
|
+
"\e[0m"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
private
|
|
204
|
+
|
|
205
|
+
def extract_heading_parts(text)
|
|
206
|
+
image_re = /((?:\e_.*?\e\\)+(?:\e\]8;;[^\a]*\a.*?\e\]8;;\a)?\n?)/m
|
|
207
|
+
parts = text.split(image_re).reject(&:empty?)
|
|
208
|
+
before = +''
|
|
209
|
+
after = +''
|
|
210
|
+
text_parts = []
|
|
211
|
+
found_text = false
|
|
212
|
+
parts.each do |part|
|
|
213
|
+
if part.match?(/\e_/)
|
|
214
|
+
found_text ? after << part : before << part
|
|
215
|
+
else
|
|
216
|
+
stripped = part.strip
|
|
217
|
+
next if stripped.empty?
|
|
218
|
+
found_text = true
|
|
219
|
+
text_parts << stripped
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
[before, after, text_parts.join(' ')]
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def parse_table_rows(str)
|
|
226
|
+
str.split("\x01").filter_map do |row_str|
|
|
227
|
+
cells = row_str.split("\x00")
|
|
228
|
+
cells.pop if cells.last&.empty?
|
|
229
|
+
cells unless cells.empty?
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def visible_length(str)
|
|
234
|
+
stripped = str.gsub(/\e\]8;;[^\a]*\a/, '').gsub(/\e\[[0-9;]*m/, '').gsub(/\e_G[^\e]*\e\\/, '')
|
|
235
|
+
stripped.each_char.sum { |c| wide_char?(c) ? 2 : 1 }
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def wide_char?(c)
|
|
239
|
+
return true if /\p{Emoji_Presentation}/.match?(c)
|
|
240
|
+
|
|
241
|
+
cp = c.ord
|
|
242
|
+
(cp >= 0x1100 && cp <= 0x115F) ||
|
|
243
|
+
(cp >= 0x2E80 && cp <= 0x9FFF) ||
|
|
244
|
+
(cp >= 0xAC00 && cp <= 0xD7A3) ||
|
|
245
|
+
(cp >= 0xF900 && cp <= 0xFAFF) ||
|
|
246
|
+
(cp >= 0xFE10 && cp <= 0xFE19) ||
|
|
247
|
+
(cp >= 0xFE30 && cp <= 0xFE6F) ||
|
|
248
|
+
(cp >= 0xFF01 && cp <= 0xFF60) ||
|
|
249
|
+
(cp >= 0xFFE0 && cp <= 0xFFE6) ||
|
|
250
|
+
(cp >= 0x20000 && cp <= 0x2FFFD) ||
|
|
251
|
+
(cp >= 0x30000 && cp <= 0x3FFFD)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def table_border(col_widths, left, mid, right)
|
|
255
|
+
"#{left}#{col_widths.map { |w| '─' * (w + 2) }.join(mid)}#{right}\n"
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def table_data_row(row, col_widths, alignments, bold:)
|
|
259
|
+
cells = col_widths.each_with_index.map do |w, i|
|
|
260
|
+
content = row[i] || ''
|
|
261
|
+
padded = align_cell(content, w, alignments[i])
|
|
262
|
+
bold ? " \e[1m#{padded}\e[22m " : " #{padded} "
|
|
263
|
+
end
|
|
264
|
+
"│#{cells.join('│')}│\n"
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def align_cell(content, width, alignment)
|
|
268
|
+
padding = width - visible_length(content)
|
|
269
|
+
return content if padding <= 0
|
|
270
|
+
|
|
271
|
+
case alignment
|
|
272
|
+
when :right
|
|
273
|
+
"#{' ' * padding}#{content}"
|
|
274
|
+
when :center
|
|
275
|
+
left = padding / 2
|
|
276
|
+
right = padding - left
|
|
277
|
+
"#{' ' * left}#{content}#{' ' * right}"
|
|
278
|
+
else
|
|
279
|
+
"#{content}#{' ' * padding}"
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def detect_text_sizing
|
|
284
|
+
con = IO.console
|
|
285
|
+
return false unless con
|
|
286
|
+
|
|
287
|
+
con.raw do |c|
|
|
288
|
+
pos1 = query_cursor_position(c)
|
|
289
|
+
return false unless pos1
|
|
290
|
+
|
|
291
|
+
c.write("\e]66;s=2;X\a")
|
|
292
|
+
|
|
293
|
+
pos2 = query_cursor_position(c)
|
|
294
|
+
return false unless pos2
|
|
295
|
+
|
|
296
|
+
supported = pos1 != pos2
|
|
297
|
+
c.write("\e[#{pos1[0]};#{pos1[1]}H\e[J") if supported
|
|
298
|
+
supported
|
|
299
|
+
end
|
|
300
|
+
rescue
|
|
301
|
+
false
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def query_cursor_position(io)
|
|
305
|
+
io.write("\e[6n")
|
|
306
|
+
buf = +''
|
|
307
|
+
while IO.select([io], nil, nil, 0.1)
|
|
308
|
+
buf << io.read_nonblock(64)
|
|
309
|
+
return [$1.to_i, $2.to_i] if buf =~ /\e\[(\d+);(\d+)R/
|
|
310
|
+
end
|
|
311
|
+
nil
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def kitty_sized(text, scale:)
|
|
315
|
+
"\e]66;s=#{scale};#{text}\a"
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def kitty_image(path)
|
|
319
|
+
encoded_path = [path].pack('m0')
|
|
320
|
+
"\e_Ga=T,f=100,t=f;#{encoded_path}\e\\"
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def rsvg_convert(svg_data)
|
|
324
|
+
IO.popen(['rsvg-convert', '--format=png'], 'r+b') do |io|
|
|
325
|
+
io.write(svg_data)
|
|
326
|
+
io.close_write
|
|
327
|
+
io.read
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def kitty_image_data(data)
|
|
332
|
+
encoded = [data].pack('m0')
|
|
333
|
+
chunks = encoded.scan(/.{1,4096}/)
|
|
334
|
+
if chunks.size == 1
|
|
335
|
+
"\e_Ga=T,f=100,t=d;#{chunks[0]}\e\\"
|
|
336
|
+
else
|
|
337
|
+
result = +"\e_Ga=T,f=100,t=d,m=1;#{chunks[0]}\e\\"
|
|
338
|
+
chunks[1...-1].each { |chunk| result << "\e_Gm=1;#{chunk}\e\\" }
|
|
339
|
+
result << "\e_Gm=0;#{chunks[-1]}\e\\"
|
|
340
|
+
result
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def png_dimensions(data)
|
|
345
|
+
return unless data.byteslice(0, 8) == "\x89PNG\r\n\x1a\n".b
|
|
346
|
+
|
|
347
|
+
[data.byteslice(16, 4).unpack1('N'), data.byteslice(20, 4).unpack1('N')]
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def cell_size
|
|
351
|
+
buf = [0].pack('S!') * 4
|
|
352
|
+
fd = IO.sysopen('/dev/tty', IO::RDWR)
|
|
353
|
+
io = IO.new(fd)
|
|
354
|
+
io.ioctl(0x40087468, buf) # TIOCGWINSZ
|
|
355
|
+
io.close
|
|
356
|
+
rows, cols, xpixel, ypixel = buf.unpack('S!4')
|
|
357
|
+
cw = cols > 0 && xpixel > 0 ? xpixel / cols : 8
|
|
358
|
+
ch = rows > 0 && ypixel > 0 ? ypixel / rows : 16
|
|
359
|
+
[cw, ch]
|
|
360
|
+
rescue
|
|
361
|
+
[8, 16]
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def image_cells(width, height)
|
|
365
|
+
cw, ch = cell_size
|
|
366
|
+
[(width.to_f / cw).ceil, (height.to_f / ch).ceil]
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
end
|
data/lib/mkd/version.rb
ADDED
data/lib/mkd.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'emoji'
|
|
4
|
+
require_relative 'mkd/version'
|
|
5
|
+
require_relative 'mkd/renderer'
|
|
6
|
+
|
|
7
|
+
module Mkd
|
|
8
|
+
def self.render(markdown_text)
|
|
9
|
+
markdown_text = markdown_text.gsub(/:(\w+):/) { Emoji.find_by_alias($1)&.raw || $& }
|
|
10
|
+
renderer = Renderer.new
|
|
11
|
+
markdown = Redcarpet::Markdown.new(renderer, fenced_code_blocks: true, strikethrough: true, no_intra_emphasis: true, autolink: true, tables: true, superscript: true, lax_spacing: true)
|
|
12
|
+
markdown.render(markdown_text)
|
|
13
|
+
end
|
|
14
|
+
end
|
data/screenshot.png
ADDED
|
Binary file
|
data/sig/mkd.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mkd
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Akira Matsuda
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: gemoji
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: redcarpet
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rouge
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
description: Renders Markdown in the terminal using ANSI escape codes and the kitty
|
|
55
|
+
text sizing protocol for headings.
|
|
56
|
+
email:
|
|
57
|
+
- ronnie@dio.jp
|
|
58
|
+
executables:
|
|
59
|
+
- mkd
|
|
60
|
+
extensions: []
|
|
61
|
+
extra_rdoc_files: []
|
|
62
|
+
files:
|
|
63
|
+
- LICENSE.txt
|
|
64
|
+
- README.md
|
|
65
|
+
- Rakefile
|
|
66
|
+
- exe/mkd
|
|
67
|
+
- lib/mkd.rb
|
|
68
|
+
- lib/mkd/renderer.rb
|
|
69
|
+
- lib/mkd/version.rb
|
|
70
|
+
- screenshot.png
|
|
71
|
+
- sig/mkd.rbs
|
|
72
|
+
homepage: https://github.com/amatsuda/mkd
|
|
73
|
+
licenses:
|
|
74
|
+
- MIT
|
|
75
|
+
metadata:
|
|
76
|
+
source_code_uri: https://github.com/amatsuda/mkd
|
|
77
|
+
homepage_uri: https://github.com/amatsuda/mkd
|
|
78
|
+
rubygems_mfa_required: 'true'
|
|
79
|
+
rdoc_options: []
|
|
80
|
+
require_paths:
|
|
81
|
+
- lib
|
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
83
|
+
requirements:
|
|
84
|
+
- - ">="
|
|
85
|
+
- !ruby/object:Gem::Version
|
|
86
|
+
version: '0'
|
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
88
|
+
requirements:
|
|
89
|
+
- - ">="
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: '0'
|
|
92
|
+
requirements: []
|
|
93
|
+
rubygems_version: 4.1.0.dev
|
|
94
|
+
specification_version: 4
|
|
95
|
+
summary: Terminal Markdown viewer
|
|
96
|
+
test_files: []
|