marvi 0.4.1 → 0.5.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 +4 -4
- data/CHANGELOG.md +12 -0
- data/lib/marvi/ast_walker.rb +69 -30
- data/lib/marvi/highlighter.rb +81 -0
- data/lib/marvi/renderer/curses.rb +26 -6
- data/lib/marvi/version.rb +1 -1
- data/lib/marvi.rb +1 -0
- metadata +22 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 99ac52daecfd730bdd05ef63523075b5e4ad3229637208a99a083199518864dd
|
|
4
|
+
data.tar.gz: 4daae68331c1edf707b6c520c3defe941e9273b937e6618711b145e13568eed4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 775e51510484dac676928c537175db222f682d3d66c74691e248616442f042f294ae896c270531475e1cdc989dde1c1a5cb957df6acdf4159e0157b4d51f3d1f
|
|
7
|
+
data.tar.gz: b00bffa8ef8adaa9e5dab84df060e31d96ffad92e473bfb544df2962c3986cb26db06c35299902a4e341dd9c91422642b06ea9e3ab15e1c8089e64f64f352d56
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.5.0] - 2026-06-02
|
|
4
|
+
|
|
5
|
+
- Syntax-highlight fenced code blocks via [Rouge](https://github.com/rouge-ruby/rouge). Blocks without a language (or with an unknown one) fall back to the previous single-color rendering.
|
|
6
|
+
- Extend the dark code block background to the longest line so the block reads as a solid pane instead of a ragged shape.
|
|
7
|
+
|
|
8
|
+
## [0.4.2] - 2026-05-31
|
|
9
|
+
|
|
10
|
+
- Add left/right padding in the curses pager so content no longer sits flush against the terminal edges. Padding scales with terminal width.
|
|
11
|
+
- Render every line of a multi-line blockquote with its `│ ` prefix.
|
|
12
|
+
- Stop emitting an empty `│ ` line at the end of blockquotes.
|
|
13
|
+
- Collapse consecutive blank lines so block-level elements are separated by a single blank line.
|
|
14
|
+
|
|
3
15
|
## [0.4.1] - 2026-05-26
|
|
4
16
|
|
|
5
17
|
- Wrap long text in bulleted/ordered lists, paragraphs, headers, and blockquotes so it no longer overflows the terminal width. List items and headers use a hanging indent for continuation lines. (#1)
|
data/lib/marvi/ast_walker.rb
CHANGED
|
@@ -16,7 +16,7 @@ module Marvi
|
|
|
16
16
|
doc = Kramdown::Document.new(markdown, input: "GFM")
|
|
17
17
|
lines = render_block(doc.root)
|
|
18
18
|
lines.pop while lines.last&.plain_text&.empty?
|
|
19
|
-
lines
|
|
19
|
+
collapse_consecutive_blanks(lines)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
private
|
|
@@ -112,11 +112,24 @@ module Marvi
|
|
|
112
112
|
lang = el.options[:lang]
|
|
113
113
|
lines = []
|
|
114
114
|
lines << RichLine.new([Span.new(text: lang, color: :yellow)], source_line: src) if lang
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
|
|
116
|
+
code = el.value.chomp
|
|
117
|
+
unless code.empty?
|
|
118
|
+
line_offset = lang ? 1 : 0
|
|
119
|
+
indent = Span.new(text: " ", bg_color: :dark)
|
|
120
|
+
indent_width = spans_display_width([indent])
|
|
121
|
+
|
|
122
|
+
highlighted = Highlighter.lines(code, lang)
|
|
123
|
+
content_widths = highlighted.map { |spans| spans_display_width(spans) }
|
|
124
|
+
block_width = [content_widths.max || 0, @max_width - indent_width].min
|
|
125
|
+
|
|
126
|
+
highlighted.each_with_index do |spans, i|
|
|
127
|
+
line_src = src ? src + i + line_offset : nil
|
|
128
|
+
line_spans = [indent] + spans
|
|
129
|
+
pad = block_width - content_widths[i]
|
|
130
|
+
line_spans << Span.new(text: " " * pad, bg_color: :dark) if pad > 0
|
|
131
|
+
lines << RichLine.new(line_spans, source_line: line_src)
|
|
118
132
|
end
|
|
119
|
-
lines << RichLine.new([Span.new(text: " #{line}", color: :green, bg_color: :dark)], source_line: line_src)
|
|
120
133
|
end
|
|
121
134
|
lines << RichLine.blank
|
|
122
135
|
lines
|
|
@@ -131,9 +144,26 @@ module Marvi
|
|
|
131
144
|
@max_width = [saved_width - prefix_width, MIN_COL_WIDTH].max
|
|
132
145
|
inner = el.children.flat_map { |child| render_block(child) }
|
|
133
146
|
@max_width = saved_width
|
|
147
|
+
# Trim trailing blanks and collapse runs of blanks so the │ prefix doesn't produce
|
|
148
|
+
# dangling/duplicated "│ " lines (kramdown emits both :p trailing blanks and explicit
|
|
149
|
+
# :blank elements between siblings).
|
|
150
|
+
inner.pop while inner.last&.plain_text&.empty?
|
|
151
|
+
inner = collapse_consecutive_blanks(inner)
|
|
134
152
|
inner.map { |line| RichLine.new([prefix] + line.spans, source_line: line.source_line) } + [RichLine.blank]
|
|
135
153
|
end
|
|
136
154
|
|
|
155
|
+
def collapse_consecutive_blanks(lines)
|
|
156
|
+
out = []
|
|
157
|
+
prev_blank = false
|
|
158
|
+
lines.each do |line|
|
|
159
|
+
is_blank = line.plain_text.empty?
|
|
160
|
+
next if is_blank && prev_blank
|
|
161
|
+
out << line
|
|
162
|
+
prev_blank = is_blank
|
|
163
|
+
end
|
|
164
|
+
out
|
|
165
|
+
end
|
|
166
|
+
|
|
137
167
|
def render_table(el)
|
|
138
168
|
src = el.options[:location]
|
|
139
169
|
rows = el.children.flat_map(&:children)
|
|
@@ -210,34 +240,43 @@ module Marvi
|
|
|
210
240
|
lines = [[]]
|
|
211
241
|
current_width = 0
|
|
212
242
|
spans.each do |span|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
243
|
+
# Split on \n so embedded hard line breaks become separate RichLines downstream,
|
|
244
|
+
# otherwise Curses.addstr resets the cursor to column 0 and bypasses padding/prefix.
|
|
245
|
+
parts = span.text.split("\n", -1)
|
|
246
|
+
parts.each_with_index do |part, idx|
|
|
247
|
+
if idx > 0 && !lines.last.empty?
|
|
217
248
|
lines << []
|
|
218
249
|
current_width = 0
|
|
219
|
-
remaining = width
|
|
220
|
-
end
|
|
221
|
-
taken = 0
|
|
222
|
-
chunk_width = 0
|
|
223
|
-
text.each_char do |c|
|
|
224
|
-
cw = Unicode::DisplayWidth.of(c)
|
|
225
|
-
break if chunk_width + cw > remaining
|
|
226
|
-
chunk_width += cw
|
|
227
|
-
taken += c.bytesize
|
|
228
250
|
end
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
251
|
+
text = part.dup
|
|
252
|
+
until text.empty?
|
|
253
|
+
remaining = width - current_width
|
|
254
|
+
if remaining <= 0
|
|
255
|
+
lines << []
|
|
256
|
+
current_width = 0
|
|
257
|
+
remaining = width
|
|
258
|
+
end
|
|
259
|
+
taken = 0
|
|
260
|
+
chunk_width = 0
|
|
261
|
+
text.each_char do |c|
|
|
262
|
+
cw = Unicode::DisplayWidth.of(c)
|
|
263
|
+
break if chunk_width + cw > remaining
|
|
264
|
+
chunk_width += cw
|
|
265
|
+
taken += c.bytesize
|
|
266
|
+
end
|
|
267
|
+
if taken.zero?
|
|
268
|
+
first_char = text.each_char.first
|
|
269
|
+
taken = first_char.bytesize
|
|
270
|
+
chunk_width = Unicode::DisplayWidth.of(first_char)
|
|
271
|
+
end
|
|
272
|
+
chunk = text.byteslice(0, taken)
|
|
273
|
+
text = text.byteslice(taken..) || ""
|
|
274
|
+
lines.last << Span.new(text: chunk, bold: span.bold, italic: span.italic, color: span.color, bg_color: span.bg_color)
|
|
275
|
+
current_width += chunk_width
|
|
276
|
+
unless text.empty?
|
|
277
|
+
lines << []
|
|
278
|
+
current_width = 0
|
|
279
|
+
end
|
|
241
280
|
end
|
|
242
281
|
end
|
|
243
282
|
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rouge"
|
|
4
|
+
|
|
5
|
+
module Marvi
|
|
6
|
+
class Highlighter
|
|
7
|
+
DEFAULT_COLOR = :white
|
|
8
|
+
FALLBACK_COLOR = :green
|
|
9
|
+
|
|
10
|
+
# Ordered so longer qualnames win over their prefixes (e.g. "Literal.String" before "Literal").
|
|
11
|
+
TOKEN_COLOR_RULES = [
|
|
12
|
+
["Comment", :cyan],
|
|
13
|
+
["Keyword", :yellow],
|
|
14
|
+
["Literal.String", :green],
|
|
15
|
+
["Literal.Number", :magenta],
|
|
16
|
+
["Literal", :magenta],
|
|
17
|
+
["Name.Function", :cyan],
|
|
18
|
+
["Name.Class", :cyan],
|
|
19
|
+
["Name.Namespace", :cyan],
|
|
20
|
+
["Name.Tag", :cyan],
|
|
21
|
+
["Name.Attribute", :cyan],
|
|
22
|
+
["Name.Decorator", :cyan],
|
|
23
|
+
["Name.Constant", :magenta],
|
|
24
|
+
["Name.Builtin", :magenta],
|
|
25
|
+
["Operator", :white],
|
|
26
|
+
["Punctuation", :white],
|
|
27
|
+
["Error", :magenta]
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
COMMENT_PREFIX = "Comment"
|
|
31
|
+
|
|
32
|
+
def self.lines(code, lang, bg_color: :dark)
|
|
33
|
+
lexer = resolve_lexer(lang)
|
|
34
|
+
return fallback(code, bg_color) if lexer.nil?
|
|
35
|
+
tokens_to_lines(lexer.lex(code), bg_color)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.resolve_lexer(lang)
|
|
39
|
+
return nil if lang.nil? || lang.empty?
|
|
40
|
+
Rouge::Lexer.find(lang)
|
|
41
|
+
rescue
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.fallback(code, bg_color)
|
|
46
|
+
code.split("\n", -1).map { |line| [Span.new(text: line, color: FALLBACK_COLOR, bg_color: bg_color)] }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.tokens_to_lines(tokens, bg_color)
|
|
50
|
+
current = []
|
|
51
|
+
lines = []
|
|
52
|
+
tokens.each do |tok, val|
|
|
53
|
+
color = token_color(tok)
|
|
54
|
+
italic = comment?(tok)
|
|
55
|
+
val.split("\n", -1).each_with_index do |part, idx|
|
|
56
|
+
if idx > 0
|
|
57
|
+
lines << current
|
|
58
|
+
current = []
|
|
59
|
+
end
|
|
60
|
+
next if part.empty?
|
|
61
|
+
current << Span.new(text: part, italic: italic, color: color, bg_color: bg_color)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
lines << current
|
|
65
|
+
lines
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.token_color(token)
|
|
69
|
+
qual = token.qualname
|
|
70
|
+
TOKEN_COLOR_RULES.each do |prefix, color|
|
|
71
|
+
return color if qual == prefix || qual.start_with?("#{prefix}.")
|
|
72
|
+
end
|
|
73
|
+
DEFAULT_COLOR
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.comment?(token)
|
|
77
|
+
qual = token.qualname
|
|
78
|
+
qual == COMMENT_PREFIX || qual.start_with?("#{COMMENT_PREFIX}.")
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -12,8 +12,11 @@ module Marvi
|
|
|
12
12
|
yellow: 3,
|
|
13
13
|
magenta: 4,
|
|
14
14
|
white: 5,
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
cyan_on_dark: 6,
|
|
16
|
+
green_on_dark: 7,
|
|
17
|
+
yellow_on_dark: 8,
|
|
18
|
+
magenta_on_dark: 9,
|
|
19
|
+
white_on_dark: 10
|
|
17
20
|
}.freeze
|
|
18
21
|
|
|
19
22
|
ITALIC_ATTR = (defined?(::Curses::A_ITALIC) ? ::Curses::A_ITALIC : 0)
|
|
@@ -23,6 +26,9 @@ module Marvi
|
|
|
23
26
|
CTRL_D = 4
|
|
24
27
|
CTRL_U = 21
|
|
25
28
|
|
|
29
|
+
MIN_HORIZONTAL_PADDING = 2
|
|
30
|
+
HORIZONTAL_PADDING_DIVISOR = 12
|
|
31
|
+
|
|
26
32
|
def render(markdown, file: nil)
|
|
27
33
|
@file = file
|
|
28
34
|
@markdown = markdown
|
|
@@ -97,8 +103,11 @@ module Marvi
|
|
|
97
103
|
::Curses.init_pair(COLOR_PAIRS[:yellow], ::Curses::COLOR_YELLOW, -1)
|
|
98
104
|
::Curses.init_pair(COLOR_PAIRS[:magenta], ::Curses::COLOR_MAGENTA, -1)
|
|
99
105
|
::Curses.init_pair(COLOR_PAIRS[:white], ::Curses::COLOR_WHITE, -1)
|
|
100
|
-
::Curses.init_pair(COLOR_PAIRS[:green_on_dark], ::Curses::COLOR_GREEN, ::Curses::COLOR_BLACK)
|
|
101
106
|
::Curses.init_pair(COLOR_PAIRS[:cyan_on_dark], ::Curses::COLOR_CYAN, ::Curses::COLOR_BLACK)
|
|
107
|
+
::Curses.init_pair(COLOR_PAIRS[:green_on_dark], ::Curses::COLOR_GREEN, ::Curses::COLOR_BLACK)
|
|
108
|
+
::Curses.init_pair(COLOR_PAIRS[:yellow_on_dark], ::Curses::COLOR_YELLOW, ::Curses::COLOR_BLACK)
|
|
109
|
+
::Curses.init_pair(COLOR_PAIRS[:magenta_on_dark], ::Curses::COLOR_MAGENTA, ::Curses::COLOR_BLACK)
|
|
110
|
+
::Curses.init_pair(COLOR_PAIRS[:white_on_dark], ::Curses::COLOR_WHITE, ::Curses::COLOR_BLACK)
|
|
102
111
|
end
|
|
103
112
|
|
|
104
113
|
def handle_key(key)
|
|
@@ -127,7 +136,17 @@ module Marvi
|
|
|
127
136
|
end
|
|
128
137
|
|
|
129
138
|
def rewalk
|
|
130
|
-
@lines = ASTWalker.new.walk(@markdown, max_width:
|
|
139
|
+
@lines = ASTWalker.new.walk(@markdown, max_width: content_width)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def horizontal_padding
|
|
143
|
+
cols = ::Curses.cols
|
|
144
|
+
return 0 if cols <= MIN_HORIZONTAL_PADDING * 2
|
|
145
|
+
[MIN_HORIZONTAL_PADDING, cols / HORIZONTAL_PADDING_DIVISOR].max
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def content_width
|
|
149
|
+
[::Curses.cols - horizontal_padding * 2, 1].max
|
|
131
150
|
end
|
|
132
151
|
|
|
133
152
|
def reload_from_key
|
|
@@ -214,8 +233,9 @@ module Marvi
|
|
|
214
233
|
|
|
215
234
|
def draw
|
|
216
235
|
::Curses.clear
|
|
236
|
+
padding = horizontal_padding
|
|
217
237
|
visible_lines.each_with_index do |line, row|
|
|
218
|
-
::Curses.setpos(row,
|
|
238
|
+
::Curses.setpos(row, padding)
|
|
219
239
|
render_line(line)
|
|
220
240
|
end
|
|
221
241
|
draw_status_bar
|
|
@@ -264,7 +284,7 @@ module Marvi
|
|
|
264
284
|
attr |= ITALIC_ATTR if span.italic
|
|
265
285
|
|
|
266
286
|
pair_key = if span.bg_color == :dark
|
|
267
|
-
|
|
287
|
+
:"#{span.color || :green}_on_dark"
|
|
268
288
|
elsif span.color
|
|
269
289
|
span.color
|
|
270
290
|
end
|
data/lib/marvi/version.rb
CHANGED
data/lib/marvi.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: marvi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mitsutaka Mimura
|
|
@@ -65,6 +65,26 @@ dependencies:
|
|
|
65
65
|
- - "~>"
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
67
|
version: '3.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rouge
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '4.0'
|
|
75
|
+
- - "<"
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '6'
|
|
78
|
+
type: :runtime
|
|
79
|
+
prerelease: false
|
|
80
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
81
|
+
requirements:
|
|
82
|
+
- - ">="
|
|
83
|
+
- !ruby/object:Gem::Version
|
|
84
|
+
version: '4.0'
|
|
85
|
+
- - "<"
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '6'
|
|
68
88
|
description: Renders Markdown with ANSI colors in pipes and an interactive curses
|
|
69
89
|
pager in TTY.
|
|
70
90
|
email:
|
|
@@ -86,6 +106,7 @@ files:
|
|
|
86
106
|
- lib/marvi/ansi.rb
|
|
87
107
|
- lib/marvi/ast_walker.rb
|
|
88
108
|
- lib/marvi/document.rb
|
|
109
|
+
- lib/marvi/highlighter.rb
|
|
89
110
|
- lib/marvi/renderer/ansi.rb
|
|
90
111
|
- lib/marvi/renderer/curses.rb
|
|
91
112
|
- lib/marvi/version.rb
|