marvi 0.4.0 → 0.4.2
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 +11 -0
- data/lib/marvi/ast_walker.rb +90 -35
- data/lib/marvi/renderer/curses.rb +16 -2
- data/lib/marvi/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d5ff0488e214dd427a028dabe9d240bee43e48ac27cef895e7f8584b41695435
|
|
4
|
+
data.tar.gz: 71dcd3de576d8926b90cae6ec4b79732be3627b85922351585b624b064eafb94
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1f90d2999b6336d1fa26f8ef441c8e51e551f4f7d229633053acc9ab8418fe115bcd6ea3f8fa3b67ce678781b29198acc33fd1ed69f591611a47bc97b456901c
|
|
7
|
+
data.tar.gz: 82bcdfbe5166bcd2f48d2367da468df0290c89e831d16fa06dced445dba98a2ba0846c659a87e9f69a1dd2e68c900f9cccdaaa57790bb0f35ddcb2c4b8d8dc0e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.4.2] - 2026-05-31
|
|
4
|
+
|
|
5
|
+
- Add left/right padding in the curses pager so content no longer sits flush against the terminal edges. Padding scales with terminal width.
|
|
6
|
+
- Render every line of a multi-line blockquote with its `│ ` prefix.
|
|
7
|
+
- Stop emitting an empty `│ ` line at the end of blockquotes.
|
|
8
|
+
- Collapse consecutive blank lines so block-level elements are separated by a single blank line.
|
|
9
|
+
|
|
10
|
+
## [0.4.1] - 2026-05-26
|
|
11
|
+
|
|
12
|
+
- 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)
|
|
13
|
+
|
|
3
14
|
## [0.4.0] - 2026-05-18
|
|
4
15
|
|
|
5
16
|
- Bind `Ctrl-D` / `Ctrl-U` for vim-style half-page scrolling in the curses pager.
|
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
|
|
@@ -29,7 +29,8 @@ module Marvi
|
|
|
29
29
|
render_header(el)
|
|
30
30
|
when :p
|
|
31
31
|
src = el.options[:location]
|
|
32
|
-
|
|
32
|
+
wrapped = wrap_spans(render_inline_children(el), @max_width)
|
|
33
|
+
wrapped.each_with_index.map { |spans, i| RichLine.new(spans, source_line: (i.zero? ? src : nil)) } + [RichLine.blank]
|
|
33
34
|
when :ul
|
|
34
35
|
el.children.flat_map { |child| render_block(child, indent: indent, list_type: :ul) } + [RichLine.blank]
|
|
35
36
|
when :ol
|
|
@@ -62,12 +63,15 @@ module Marvi
|
|
|
62
63
|
content = render_inline_children(el).map do |s|
|
|
63
64
|
Span.new(text: s.text, bold: true, italic: s.italic, color: s.color || color, bg_color: s.bg_color)
|
|
64
65
|
end
|
|
65
|
-
|
|
66
|
+
wrap_with_prefix([prefix], content, @max_width, source_line: src) + [RichLine.blank]
|
|
66
67
|
end
|
|
67
68
|
|
|
68
69
|
def render_li(el, indent:, list_type:, list_index:)
|
|
69
70
|
bullet = (list_type == :ol) ? "#{list_index}." : "•"
|
|
70
71
|
prefix = Span.new(text: "#{" " * indent}#{bullet} ", color: :cyan)
|
|
72
|
+
prefix_width = spans_display_width([prefix])
|
|
73
|
+
hanging = Span.new(text: " " * prefix_width)
|
|
74
|
+
inner_width = [@max_width - prefix_width, MIN_COL_WIDTH].max
|
|
71
75
|
src = el.options[:location]
|
|
72
76
|
lines = []
|
|
73
77
|
|
|
@@ -78,16 +82,25 @@ module Marvi
|
|
|
78
82
|
nested.pop while nested.last&.plain_text&.empty?
|
|
79
83
|
lines += nested
|
|
80
84
|
when :p
|
|
85
|
+
content_spans = render_inline_children(child)
|
|
81
86
|
if lines.empty?
|
|
82
|
-
lines
|
|
87
|
+
lines += wrap_with_prefix([prefix], content_spans, @max_width, source_line: src)
|
|
83
88
|
else
|
|
84
|
-
|
|
89
|
+
child_src = child.options[:location]
|
|
90
|
+
wrapped = wrap_spans(content_spans, inner_width)
|
|
91
|
+
wrapped.each_with_index do |spans, i|
|
|
92
|
+
lines << RichLine.new([hanging] + spans, source_line: (i.zero? ? child_src : nil))
|
|
93
|
+
end
|
|
94
|
+
lines << RichLine.blank
|
|
85
95
|
end
|
|
86
96
|
else
|
|
87
|
-
|
|
88
|
-
|
|
97
|
+
content_spans = render_inline(child)
|
|
98
|
+
if lines.empty?
|
|
99
|
+
lines += wrap_with_prefix([prefix], content_spans, @max_width, source_line: src)
|
|
89
100
|
else
|
|
90
|
-
|
|
101
|
+
wrap_spans(content_spans, inner_width).each do |spans|
|
|
102
|
+
lines << RichLine.new([hanging] + spans)
|
|
103
|
+
end
|
|
91
104
|
end
|
|
92
105
|
end
|
|
93
106
|
end
|
|
@@ -110,12 +123,34 @@ module Marvi
|
|
|
110
123
|
end
|
|
111
124
|
|
|
112
125
|
def render_blockquote(el)
|
|
113
|
-
inner = el.children.flat_map { |child| render_block(child) }
|
|
114
126
|
prefix = Span.new(text: "│ ", color: :cyan)
|
|
115
|
-
|
|
127
|
+
prefix_width = spans_display_width([prefix])
|
|
128
|
+
# Reduce @max_width while rendering inner content so the │ prefix fits within @max_width
|
|
129
|
+
# without forcing a second wrap pass on already-wrapped lines.
|
|
130
|
+
saved_width = @max_width
|
|
131
|
+
@max_width = [saved_width - prefix_width, MIN_COL_WIDTH].max
|
|
132
|
+
inner = el.children.flat_map { |child| render_block(child) }
|
|
133
|
+
@max_width = saved_width
|
|
134
|
+
# Trim trailing blanks and collapse runs of blanks so the │ prefix doesn't produce
|
|
135
|
+
# dangling/duplicated "│ " lines (kramdown emits both :p trailing blanks and explicit
|
|
136
|
+
# :blank elements between siblings).
|
|
137
|
+
inner.pop while inner.last&.plain_text&.empty?
|
|
138
|
+
inner = collapse_consecutive_blanks(inner)
|
|
116
139
|
inner.map { |line| RichLine.new([prefix] + line.spans, source_line: line.source_line) } + [RichLine.blank]
|
|
117
140
|
end
|
|
118
141
|
|
|
142
|
+
def collapse_consecutive_blanks(lines)
|
|
143
|
+
out = []
|
|
144
|
+
prev_blank = false
|
|
145
|
+
lines.each do |line|
|
|
146
|
+
is_blank = line.plain_text.empty?
|
|
147
|
+
next if is_blank && prev_blank
|
|
148
|
+
out << line
|
|
149
|
+
prev_blank = is_blank
|
|
150
|
+
end
|
|
151
|
+
out
|
|
152
|
+
end
|
|
153
|
+
|
|
119
154
|
def render_table(el)
|
|
120
155
|
src = el.options[:location]
|
|
121
156
|
rows = el.children.flat_map(&:children)
|
|
@@ -177,38 +212,58 @@ module Marvi
|
|
|
177
212
|
shrunk
|
|
178
213
|
end
|
|
179
214
|
|
|
215
|
+
def wrap_with_prefix(prefix_spans, content_spans, max_width, source_line: nil)
|
|
216
|
+
prefix_width = spans_display_width(prefix_spans)
|
|
217
|
+
inner_width = [max_width - prefix_width, MIN_COL_WIDTH].max
|
|
218
|
+
wrapped = wrap_spans(content_spans, inner_width)
|
|
219
|
+
indent = Span.new(text: " " * prefix_width)
|
|
220
|
+
wrapped.each_with_index.map do |spans, i|
|
|
221
|
+
line_prefix = i.zero? ? prefix_spans : [indent]
|
|
222
|
+
RichLine.new(line_prefix + spans, source_line: (i.zero? ? source_line : nil))
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
180
226
|
def wrap_spans(spans, width)
|
|
181
227
|
lines = [[]]
|
|
182
228
|
current_width = 0
|
|
183
229
|
spans.each do |span|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
230
|
+
# Split on \n so embedded hard line breaks become separate RichLines downstream,
|
|
231
|
+
# otherwise Curses.addstr resets the cursor to column 0 and bypasses padding/prefix.
|
|
232
|
+
parts = span.text.split("\n", -1)
|
|
233
|
+
parts.each_with_index do |part, idx|
|
|
234
|
+
if idx > 0 && !lines.last.empty?
|
|
188
235
|
lines << []
|
|
189
236
|
current_width = 0
|
|
190
|
-
remaining = width
|
|
191
|
-
end
|
|
192
|
-
taken = 0
|
|
193
|
-
chunk_width = 0
|
|
194
|
-
text.each_char do |c|
|
|
195
|
-
cw = Unicode::DisplayWidth.of(c)
|
|
196
|
-
break if chunk_width + cw > remaining
|
|
197
|
-
chunk_width += cw
|
|
198
|
-
taken += c.bytesize
|
|
199
237
|
end
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
238
|
+
text = part.dup
|
|
239
|
+
until text.empty?
|
|
240
|
+
remaining = width - current_width
|
|
241
|
+
if remaining <= 0
|
|
242
|
+
lines << []
|
|
243
|
+
current_width = 0
|
|
244
|
+
remaining = width
|
|
245
|
+
end
|
|
246
|
+
taken = 0
|
|
247
|
+
chunk_width = 0
|
|
248
|
+
text.each_char do |c|
|
|
249
|
+
cw = Unicode::DisplayWidth.of(c)
|
|
250
|
+
break if chunk_width + cw > remaining
|
|
251
|
+
chunk_width += cw
|
|
252
|
+
taken += c.bytesize
|
|
253
|
+
end
|
|
254
|
+
if taken.zero?
|
|
255
|
+
first_char = text.each_char.first
|
|
256
|
+
taken = first_char.bytesize
|
|
257
|
+
chunk_width = Unicode::DisplayWidth.of(first_char)
|
|
258
|
+
end
|
|
259
|
+
chunk = text.byteslice(0, taken)
|
|
260
|
+
text = text.byteslice(taken..) || ""
|
|
261
|
+
lines.last << Span.new(text: chunk, bold: span.bold, italic: span.italic, color: span.color, bg_color: span.bg_color)
|
|
262
|
+
current_width += chunk_width
|
|
263
|
+
unless text.empty?
|
|
264
|
+
lines << []
|
|
265
|
+
current_width = 0
|
|
266
|
+
end
|
|
212
267
|
end
|
|
213
268
|
end
|
|
214
269
|
end
|
|
@@ -23,6 +23,9 @@ module Marvi
|
|
|
23
23
|
CTRL_D = 4
|
|
24
24
|
CTRL_U = 21
|
|
25
25
|
|
|
26
|
+
MIN_HORIZONTAL_PADDING = 2
|
|
27
|
+
HORIZONTAL_PADDING_DIVISOR = 12
|
|
28
|
+
|
|
26
29
|
def render(markdown, file: nil)
|
|
27
30
|
@file = file
|
|
28
31
|
@markdown = markdown
|
|
@@ -127,7 +130,17 @@ module Marvi
|
|
|
127
130
|
end
|
|
128
131
|
|
|
129
132
|
def rewalk
|
|
130
|
-
@lines = ASTWalker.new.walk(@markdown, max_width:
|
|
133
|
+
@lines = ASTWalker.new.walk(@markdown, max_width: content_width)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def horizontal_padding
|
|
137
|
+
cols = ::Curses.cols
|
|
138
|
+
return 0 if cols <= MIN_HORIZONTAL_PADDING * 2
|
|
139
|
+
[MIN_HORIZONTAL_PADDING, cols / HORIZONTAL_PADDING_DIVISOR].max
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def content_width
|
|
143
|
+
[::Curses.cols - horizontal_padding * 2, 1].max
|
|
131
144
|
end
|
|
132
145
|
|
|
133
146
|
def reload_from_key
|
|
@@ -214,8 +227,9 @@ module Marvi
|
|
|
214
227
|
|
|
215
228
|
def draw
|
|
216
229
|
::Curses.clear
|
|
230
|
+
padding = horizontal_padding
|
|
217
231
|
visible_lines.each_with_index do |line, row|
|
|
218
|
-
::Curses.setpos(row,
|
|
232
|
+
::Curses.setpos(row, padding)
|
|
219
233
|
render_line(line)
|
|
220
234
|
end
|
|
221
235
|
draw_status_bar
|
data/lib/marvi/version.rb
CHANGED