Skip to content

Commit 04e75a3

Browse files
feat: allow bullet icons to be a list of lists
## Details Request: #217 This allows each icon for a bullet to itself be a list of strings. This list is indexed into using the sibling level of the list item. Sibling level just means which item number is this in the current list. So you can do something like roman numerals for level 1 and the alphabet for level 2. The list is accessed via a clamp so if we exceed the end we keep using the last value rather than cycling back to the front. Unrelated change, add a screenshot next to the gifs, see how that looks, might remove it.
1 parent 435b1ed commit 04e75a3

File tree

14 files changed

+127
-40
lines changed

14 files changed

+127
-40
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
*.gif
2+
*.png
23
/temp/

README.md

+7-5
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ Plugin to improve viewing Markdown files in Neovim
44

55
<!-- panvimdoc-ignore-start -->
66

7-
| | |
8-
| --------- | ------- |
9-
| ![Heading](https://github.com/user-attachments/assets/6184ea2d-1769-4c37-bdc4-e6b0d1ca5c00) | ![Table](https://github.com/user-attachments/assets/328473e7-450a-4f52-bc0e-02ccc85e6268) |
10-
| ![Quote](https://github.com/user-attachments/assets/e7da67bc-7a3f-49f0-b8f6-3e61cf59197b) | ![LaTeX](https://github.com/user-attachments/assets/58da917b-5ca5-4705-9cad-978e7bb8574a) |
11-
| ![Callout](https://github.com/user-attachments/assets/73253fa4-ff4f-4562-a721-30c0a218c280) | |
7+
| Screenshot | Video |
8+
| ---------- | --------- |
9+
| ![Heading](https://github.com/user-attachments/assets/36f0fbb6-99bc-4cb2-8f69-fbea3d10abf1) | ![Heading](https://github.com/user-attachments/assets/e57a53ea-f0bf-48db-90c1-0c2365ab3c54) |
10+
| ![Table](https://github.com/user-attachments/assets/cbb81758-820d-467c-bc44-07003cb532bb) | ![Table](https://github.com/user-attachments/assets/5cd2b69d-ef17-4c6e-9510-59ed10e385d5) |
11+
| ![Quote](https://github.com/user-attachments/assets/822ae62c-bc0f-40a7-b8bb-fb3a885a95f9) | ![Quote](https://github.com/user-attachments/assets/aa002ac7-b30f-4079-bba9-505160a4ad78) |
12+
| ![Callout](https://github.com/user-attachments/assets/397ddd7b-bb82-47d0-ad9d-bdbe2f9858d7) | ![Callout](https://github.com/user-attachments/assets/250aaeda-6141-4f4c-b72c-103875ca6eb8) |
13+
| ![LaTeX](https://github.com/user-attachments/assets/7b859c0a-1bf6-4398-88b5-7bcde12f2390) | ![LaTeX](https://github.com/user-attachments/assets/9ef14030-f688-47fd-95ff-befab1253322) |
1214

1315
<!-- panvimdoc-ignore-end -->
1416

benches/readme_spec.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ local util = require('benches.util')
44

55
describe('README.md', function()
66
it('default', function()
7-
local base_marks = 66
7+
local base_marks = 73
88
util.less_than(util.setup('README.md'), 50)
99
util.num_marks(base_marks)
1010

demo/format.tape

+6-3
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,19 @@ Sleep 2s
1717

1818
WRITE
1919

20-
Type "i" Sleep 1s
21-
Escape Sleep 1s
20+
Down
21+
Type "i" Sleep 0.5s
22+
Screenshot default.png Sleep 0.5s
23+
Escape Sleep 0.5s
24+
Screenshot rendered.png Sleep 0.5s
2225

2326
MOVE
2427

2528
Type "i" Sleep 1s
2629
Escape Sleep 1s
2730

2831
# Close without editing
29-
Type ":q!"
3032
Hide
33+
Type ":q!"
3134
Enter
3235
Sleep 100ms

demo/run.py

+47-18
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from argparse import ArgumentParser
33
from pathlib import Path
44

5+
from PIL import Image
6+
57
INFO: dict[str, tuple[int, str]] = dict(
68
heading_code=(550, "## Heading 2"),
79
list_table=(550, ""),
@@ -12,31 +14,58 @@
1214

1315

1416
def main(name: str) -> None:
15-
in_file = Path(f"demo/{name}.md")
16-
assert in_file.exists()
17+
file = Path(f"demo/{name}.md")
18+
assert file.exists()
19+
20+
create_gif(name, file)
21+
create_screenshot(name)
22+
1723

18-
out_file = Path(f"demo/{name}.gif")
19-
if out_file.exists():
20-
out_file.unlink()
24+
def create_gif(name: str, file: Path) -> None:
25+
gif = Path(f"demo/{name}.gif")
26+
if gif.exists():
27+
gif.unlink()
2128

2229
height, content = INFO[name]
2330

2431
tape = Path("demo/demo.tape")
25-
tape.write_text(tape_content(in_file, out_file, height, content))
32+
tape.write_text(tape_content(file, gif, height, content))
2633
result = subprocess.run(["vhs", tape])
2734
assert result.returncode == 0
2835
tape.unlink()
2936

3037

31-
def tape_content(in_file: Path, out_file: Path, height: int, to_write: str) -> str:
32-
content = Path("demo/format.tape").read_text()
33-
content = content.replace("INPUT", str(in_file))
34-
content = content.replace("OUTPUT", str(out_file))
35-
content = content.replace("WIDTH", str(550))
36-
content = content.replace("HEIGHT", str(height))
37-
content = content.replace("WRITE", get_write(to_write))
38-
content = content.replace("MOVE", get_move(in_file))
39-
return content
38+
def create_screenshot(name: str) -> None:
39+
screenshot = Path(f"demo/{name}.png")
40+
if screenshot.exists():
41+
screenshot.unlink()
42+
43+
default, rendered = Path("default.png"), Path("rendered.png")
44+
assert default.exists() and rendered.exists()
45+
46+
left, right = Image.open(default), Image.open(rendered)
47+
48+
mode, width, height = left.mode, left.width, left.height
49+
assert mode == right.mode and width == right.width and height == right.height
50+
51+
combined = Image.new(mode, (2 * width, height))
52+
combined.paste(left, (0, 0))
53+
combined.paste(right, (width, 0))
54+
combined.save(screenshot)
55+
56+
default.unlink()
57+
rendered.unlink()
58+
59+
60+
def tape_content(file: Path, gif: Path, height: int, content: str) -> str:
61+
result = Path("demo/format.tape").read_text()
62+
result = result.replace("INPUT", str(file))
63+
result = result.replace("OUTPUT", str(gif))
64+
result = result.replace("WIDTH", str(550))
65+
result = result.replace("HEIGHT", str(height))
66+
result = result.replace("WRITE", get_write(content))
67+
result = result.replace("MOVE", get_move(file))
68+
return result
4069

4170

4271
def get_write(content: str) -> str:
@@ -49,10 +78,10 @@ def get_write(content: str) -> str:
4978
return "\n".join(write)
5079

5180

52-
def get_move(in_file: Path) -> str:
81+
def get_move(file: Path) -> str:
5382
move: list[str] = []
54-
# Get lines so we know how to scroll down, account for starting on first line
55-
lines: list[str] = Path(in_file).read_text().splitlines()[1:]
83+
# Get lines so we know how to scroll down, account for starting on second line
84+
lines: list[str] = file.read_text().splitlines()[2:]
5685
for line in lines:
5786
skip = (" ", "def", "if")
5887
duration = 0.1 if line == "" or line.startswith(skip) else 0.75

doc/render-markdown.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
*render-markdown.txt* For 0.10.0 Last change: 2024 October 24
1+
*render-markdown.txt* For 0.10.0 Last change: 2024 October 25
22

33
==============================================================================
44
Table of Contents *render-markdown-table-of-contents*

lua/render-markdown/debug/validator.lua

+30-2
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ function Spec:list(keys, list_type, input_types)
7979
elseif type(v) == 'table' then
8080
for i, item in ipairs(v) do
8181
if type(item) ~= list_type then
82-
return false, string.format('Index %d is %s', i, type(item))
82+
return false, string.format('[%d] is %s', i, type(item))
8383
end
8484
end
8585
return true
@@ -89,6 +89,34 @@ function Spec:list(keys, list_type, input_types)
8989
end, list_type .. ' list' .. suffix)
9090
end
9191

92+
---@param keys string|string[]
93+
---@param list_type type
94+
---@param input_types? type|type[]
95+
---@return render.md.debug.ValidatorSpec
96+
function Spec:list_or_list_of_list(keys, list_type, input_types)
97+
local types, suffix = self:handle_types(input_types)
98+
return self:add(keys, function(v)
99+
if vim.tbl_contains(types, type(v)) then
100+
return true
101+
elseif type(v) == 'table' then
102+
for i, item in ipairs(v) do
103+
if type(item) == 'table' then
104+
for j, nested in ipairs(item) do
105+
if type(nested) ~= list_type then
106+
return false, string.format('[%d][%d] is %s', i, j, type(nested))
107+
end
108+
end
109+
elseif type(item) ~= list_type then
110+
return false, string.format('[%d] is %s', i, type(item))
111+
end
112+
end
113+
return true
114+
else
115+
return false
116+
end
117+
end, list_type .. ' list or list of list' .. suffix)
118+
end
119+
92120
---@param keys string|string[]
93121
---@param values string[]
94122
---@param input_types? type|type[]
@@ -103,7 +131,7 @@ function Spec:one_or_list_of(keys, values, input_types)
103131
elseif type(v) == 'table' then
104132
for i, item in ipairs(v) do
105133
if not vim.tbl_contains(values, item) then
106-
return false, string.format('Index %d is %s', i, item)
134+
return false, string.format('[%d] is %s', i, item)
107135
end
108136
end
109137
return true

lua/render-markdown/health.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ local state = require('render-markdown.state')
44
local M = {}
55

66
---@private
7-
M.version = '7.4.7'
7+
M.version = '7.4.8'
88

99
function M.check()
1010
M.start('version')

lua/render-markdown/init.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ local M = {}
110110

111111
---@class (exact) render.md.UserBullet
112112
---@field public enabled? boolean
113-
---@field public icons? string[]
113+
---@field public icons? (string|string[])[]
114114
---@field public left_pad? integer
115115
---@field public right_pad? integer
116116
---@field public highlight? string

lua/render-markdown/lib/node.lua

+11
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,17 @@ function Node:sibling(target)
103103
return nil
104104
end
105105

106+
---@param target string
107+
---@return integer
108+
function Node:sibling_count(target)
109+
local count, sibling = 1, self.node:prev_sibling()
110+
while sibling ~= nil and sibling:type() == target do
111+
count = count + 1
112+
sibling = sibling:prev_sibling()
113+
end
114+
return count
115+
end
116+
106117
---@param target_type string
107118
---@param target_row? integer
108119
---@return render.md.Node?

lua/render-markdown/render/list_marker.lua

+18-5
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ local List = require('render-markdown.lib.list')
33
local Str = require('render-markdown.lib.str')
44

55
---@class render.md.data.ListMarker
6-
---@field leading_spaces integer
6+
---@field spaces integer
77
---@field checkbox? render.md.CustomCheckbox
88

99
---@class render.md.render.ListMarker: render.md.Renderer
@@ -20,7 +20,7 @@ function Render:setup()
2020
-- List markers from tree-sitter should have leading spaces removed, however there are edge
2121
-- cases in the parser: https://github.com/tree-sitter-grammars/tree-sitter-markdown/issues/127
2222
-- As a result we account for leading spaces here, can remove if this gets fixed upstream
23-
leading_spaces = Str.spaces('start', self.node.text),
23+
spaces = Str.spaces('start', self.node.text),
2424
checkbox = self.context:get_checkbox(self.node),
2525
}
2626

@@ -62,7 +62,7 @@ end
6262

6363
---@private
6464
function Render:hide_marker()
65-
self.marks:add('check_icon', self.node.start_row, self.node.start_col + self.data.leading_spaces, {
65+
self.marks:add('check_icon', self.node.start_row, self.node.start_col + self.data.spaces, {
6666
end_row = self.node.end_row,
6767
end_col = self.node.end_col,
6868
conceal = '',
@@ -81,14 +81,27 @@ end
8181
---@param level integer
8282
function Render:icon(level)
8383
local icon = List.cycle(self.bullet.icons, level)
84+
if type(icon) == 'table' then
85+
local item = self.node:parent('list_item')
86+
if item == nil then
87+
return
88+
end
89+
icon = List.clamp(icon, item:sibling_count('list_item'))
90+
end
8491
if icon == nil then
8592
return
8693
end
94+
local text = Str.pad(self.data.spaces) .. icon
95+
local position, conceal = 'overlay', nil
96+
if Str.width(text) > Str.width(self.node.text) then
97+
position, conceal = 'inline', ''
98+
end
8799
self.marks:add('bullet', self.node.start_row, self.node.start_col, {
88100
end_row = self.node.end_row,
89101
end_col = self.node.end_col,
90-
virt_text = { { Str.pad(self.data.leading_spaces) .. icon, self.bullet.highlight } },
91-
virt_text_pos = 'overlay',
102+
virt_text = { { text, self.bullet.highlight } },
103+
virt_text_pos = position,
104+
conceal = conceal,
92105
})
93106
end
94107

lua/render-markdown/state.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ function M.validate()
183183
:type('enabled', 'boolean')
184184
:type({ 'left_pad', 'right_pad' }, 'number')
185185
:type('highlight', 'string')
186-
:list('icons', 'string')
186+
:list_or_list_of_list('icons', 'string')
187187
:check()
188188

189189
get_spec('checkbox')

lua/render-markdown/types.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191

9292
---@class (exact) render.md.Bullet
9393
---@field public enabled boolean
94-
---@field public icons string[]
94+
---@field public icons (string|string[])[]
9595
---@field public left_pad integer
9696
---@field public right_pad integer
9797
---@field public highlight string

tests/state_spec.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ describe('state', function()
8686
prefix
8787
.. '.render_modes: expected string list or type { "boolean" }, got '
8888
.. tostring(int_render_modes)
89-
.. '. Info: Index 1 is number',
89+
.. '. Info: [1] is number',
9090
}, validate({ render_modes = int_render_modes }))
9191

9292
eq(

0 commit comments

Comments
 (0)