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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b0110473458c01229add5274f0ab0df77dad55587286ee01c426612fa0505707
4
- data.tar.gz: 8e59b8386b4fb283613a2fc81d2841473ccd78eaa216a4608e10c17ae62c61b0
3
+ metadata.gz: d5ff0488e214dd427a028dabe9d240bee43e48ac27cef895e7f8584b41695435
4
+ data.tar.gz: 71dcd3de576d8926b90cae6ec4b79732be3627b85922351585b624b064eafb94
5
5
  SHA512:
6
- metadata.gz: 3abaf2190555600e5e04cd0a04d7176d6ef5b8384afc902cff76389ec782a448bd99609cf2474d7f62827bc24750514b7517cf780b61739c327a665bb3cc9821
7
- data.tar.gz: 2caddfb8de48b8fc5f6c894f839d06239b57845993a67f576f8cde2cb6591509d3167dbe35d11636e1c03f5cb140e2c16cfeec945bf8204eeb4095f3a31c8bcb
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.
@@ -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
- [RichLine.new(render_inline_children(el), source_line: src), RichLine.blank]
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
- [RichLine.new([prefix] + content, source_line: src), RichLine.blank]
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 << RichLine.new([prefix] + render_inline_children(child), source_line: src)
87
+ lines += wrap_with_prefix([prefix], content_spans, @max_width, source_line: src)
83
88
  else
84
- lines += render_block(child)
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
- lines << if lines.empty?
88
- RichLine.new([prefix] + render_inline(child), source_line: src)
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
- RichLine.new(render_inline(child))
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
- # preserve source_line from inner lines
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
- text = span.text.dup
185
- until text.empty?
186
- remaining = width - current_width
187
- if remaining <= 0
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
- if taken.zero?
201
- first_char = text.each_char.first
202
- taken = first_char.bytesize
203
- chunk_width = Unicode::DisplayWidth.of(first_char)
204
- end
205
- chunk = text.byteslice(0, taken)
206
- text = text.byteslice(taken..) || ""
207
- lines.last << Span.new(text: chunk, bold: span.bold, italic: span.italic, color: span.color, bg_color: span.bg_color)
208
- current_width += chunk_width
209
- unless text.empty?
210
- lines << []
211
- current_width = 0
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: ::Curses.cols)
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, 0)
232
+ ::Curses.setpos(row, padding)
219
233
  render_line(line)
220
234
  end
221
235
  draw_status_bar
data/lib/marvi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Marvi
4
- VERSION = "0.4.0"
4
+ VERSION = "0.4.2"
5
5
  end
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.0
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mitsutaka Mimura