kramdown-ansi 0.5.0 → 0.7.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.
@@ -0,0 +1,345 @@
1
+ ╭───┬───╮
2
+ │ A │ B │
3
+ ├───┼───┤
4
+ │ 1 │ 2 │
5
+ ╰───┴───╯
6
+
7
+ Case 00 — Table at Document Start
8
+
9
+ Case 01 — Column Count Mismatch
10
+
11
+ Header has 3 columns, data rows have 5 and 1.
12
+
13
+ ╭──────────┬───┬───┬───┬───╮
14
+ │ A │ B │ C │ │ │
15
+ ├──────────┼───┼───┼───┼───┤
16
+ │ 1 │ 2 │ 3 │ 4 │ 5 │
17
+ ├──────────┼───┼───┼───┼───┤
18
+ │ only-one │ │ │ │ │
19
+ ├──────────┼───┼───┼───┼───┤
20
+ │ a │ b │ c │ d │ │
21
+ ╰──────────┴───┴───┴───┴───╯
22
+
23
+ Case 02 — No Leading / Trailing Pipes
24
+
25
+ GFM allows omitting the outer |. Renderer must still split on inner
26
+ pipes.
27
+
28
+ ╭───┬───┬───╮
29
+ │ A │ B │ C │
30
+ ├───┼───┼───┤
31
+ │ 1 │ 2 │ 3 │
32
+ ├───┼───┼───┤
33
+ │ 4 │ 5 │ 6 │
34
+ ╰───┴───┴───╯
35
+
36
+ Case 03 — Escaped Pipe in Cell
37
+
38
+ \| should render as a literal |, not split the column.
39
+
40
+ ╭───────────────────────┬───────╮
41
+ │ Col │ Value │
42
+ ├───────────────────────┼───────┤
43
+ │ a\|b │ 1 │
44
+ ├───────────────────────┼───────┤
45
+ │ x \| y │ 2 │
46
+ ├───────────────────────┼───────┤
47
+ │ no-escape-but-literal │ pipe │
48
+ ╰───────────────────────┴───────╯
49
+
50
+ Case 04 — Empty Cells
51
+
52
+ ╭──────┬─────┬───────╮
53
+ │ A │ B │ C │
54
+ ├──────┼─────┼───────┤
55
+ │ │ │ │
56
+ ├──────┼─────┼───────┤
57
+ │ left │ │ right │
58
+ ├──────┼─────┼───────┤
59
+ │ │ mid │ │
60
+ ├──────┼─────┼───────┤
61
+ │ │ │ │
62
+ ╰──────┴─────┴───────╯
63
+
64
+ Case 05 — Single-Column Table
65
+
66
+ ╭───────╮
67
+ │ Only │
68
+ ├───────┤
69
+ │ one │
70
+ ├───────┤
71
+ │ two │
72
+ ├───────┤
73
+ │ three │
74
+ ╰───────╯
75
+
76
+ Case 06 — Header + Alignment Only (No Data Rows)
77
+
78
+ not a table
79
+
80
+ | X | Y | Z |
81
+ |:-:|:-:|:-:|
82
+
83
+ Case 07 — Back-to-Back Tables (No Blank Line)
84
+
85
+ ╭─────┬─────╮
86
+ │ T1a │ T1b │
87
+ ├─────┼─────┤
88
+ │ a │ b │
89
+ ╰─────┴─────╯
90
+
91
+ ╭─────┬─────╮
92
+ │ T2a │ T2b │
93
+ ├─────┼─────┤
94
+ │ c │ d │
95
+ ╰─────┴─────╯
96
+
97
+ Case 08 — Full-Width / Variable-Width Unicode
98
+
99
+ CJK, emoji (double-width), combining marks — column widths will lie.
100
+
101
+ ╭─────────┬─────────┬──────────────────────────╮
102
+ │ Char │ Width │ Note │
103
+ ├─────────┼─────────┼──────────────────────────┤
104
+ │ 你 │ 2 cells │ CJK │
105
+ ├─────────┼─────────┼──────────────────────────┤
106
+ │ 💜 │ 2 cells │ Emoji │
107
+ ├─────────┼─────────┼──────────────────────────┤
108
+ │ e\u0301 │ 1+0 │ Combining acute │
109
+ ├─────────┼─────────┼──────────────────────────┤
110
+ │ ☝ │ 2 cells │ DBCS symbol │
111
+ ├─────────┼─────────┼──────────────────────────┤
112
+ │ | │ 1 cell │ Looks like a pipe, isn't │
113
+ ╰─────────┴─────────┴──────────────────────────╯
114
+
115
+ Case 09 — Markdown Inside Cells
116
+
117
+ Bold, italic, code, links, strikethrough, nested.
118
+
119
+ ╭───────────────────┬─────────╮
120
+ │ Cell │ Renders │
121
+ ├───────────────────┼─────────┤
122
+ │ **bold** │ bold │
123
+ ├───────────────────┼─────────┤
124
+ │ *it* │ it │
125
+ ├───────────────────┼─────────┤
126
+ │ `code` │ code │
127
+ ├───────────────────┼─────────┤
128
+ │ [link](https://x) │ ]8;;https://x\link]8;;\ │
129
+ ├───────────────────┼─────────┤
130
+ │ ~~gone~~ │ gone │
131
+ ├───────────────────┼─────────┤
132
+ │ **_both_** │ both │
133
+ ╰───────────────────┴─────────╯
134
+
135
+ Case 10 — Alignment Row Has Wrong Column Count
136
+
137
+ | A | B | C |
138
+ |:-:|:-:|
139
+
140
+ Case 11 — Table Split by Blank Line
141
+
142
+ ╭───┬───╮
143
+ │ A │ B │
144
+ ├───┼───┤
145
+ │ 1 │ 2 │
146
+ ╰───┴───╯
147
+
148
+ ╭───┬───╮
149
+ │ A │ B │
150
+ ├───┼───┤
151
+ │ 3 │ 4 │
152
+ ╰───┴───╯
153
+
154
+ Case 12 — Excessive Whitespace Padding
155
+
156
+ ╭───┬───┬───╮
157
+ │ A │ B │ C │
158
+ ├───┼───┼───┤
159
+ │ 1 │ 2 │ 3 │
160
+ ╰───┴───┴───╯
161
+
162
+ Case 13 — <br> and HTML in Cells
163
+
164
+ ╭──────┬─────────────────────╮
165
+ │ Cell │ Content │
166
+ ├──────┼─────────────────────┤
167
+ │ br │ line1line2 │
168
+ ├──────┼─────────────────────┤
169
+ │ html │ <span>inline</span> │
170
+ ├──────┼─────────────────────┤
171
+ │ both │ bold italic │
172
+ ╰──────┴─────────────────────╯
173
+
174
+ Case 14 — Pipe at Both Ends, Nothing Between
175
+
176
+ ╭───╮
177
+ │ │
178
+ ├───┤
179
+ │ │
180
+ ╰───╯
181
+
182
+ Case 15 — One Data Row, 10 Columns
183
+
184
+ ╭───┬───┬───┬───┬───┬───┬───┬───┬───┬────╮
185
+ │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │
186
+ ├───┼───┼───┼───┼───┼───┼───┼───┼───┼────┤
187
+ │ a │ b │ c │ d │ e │ f │ g │ h │ i │ j │
188
+ ╰───┴───┴───┴───┴───┴───┴───┴───┴───┴────╯
189
+
190
+ Case 16 — Alignment Row with Mixed/Invalid Markers
191
+
192
+ ╭───┬───┬───╮
193
+ │ A │ B │ C │
194
+ ├───┼───┼───┤
195
+ │ l │ r │ d │
196
+ ╰───┴───┴───╯
197
+
198
+ Case 17 — Table at Document Start, Glued to Following Header
199
+
200
+ ╭───┬───╮
201
+ │ a │ b │
202
+ ├───┼───┤
203
+ │ 1 │ 2 │
204
+ ╰───┴───╯
205
+
206
+ next section
207
+
208
+ Case 18 — Table Glued to Preceding Header
209
+
210
+ heading
211
+
212
+ ╭───┬───╮
213
+ │ a │ b │
214
+ ├───┼───┤
215
+ │ 1 │ 2 │
216
+ ╰───┴───╯
217
+
218
+ Case 19 — Single Column, No Trailing Pipe on Header
219
+
220
+ ╭─────────╮
221
+ │ a table │
222
+ ├─────────┤
223
+ │ hi │
224
+ ╰─────────╯
225
+
226
+ Case 20 — Not a Table: Single Pipe Line
227
+
228
+ not a table
229
+
230
+ Case 21 — Not a Table: Header + Sep, No Body, Missing Closing Pipe
231
+
232
+ |not a table
233
+ |-|
234
+
235
+ Case 22 — Table After Code Block, Glued
236
+
237
+ some code
238
+ 
239
+ ╭───┬───╮
240
+ │ a │ b │
241
+ ├───┼───┤
242
+ │ 1 │ 2 │
243
+ ╰───┴───╯
244
+
245
+ Case 23 — Table After List Item, Glued
246
+
247
+ · item one
248
+
249
+ · item two
250
+
251
+ ╭───┬───╮
252
+ │ a │ b │
253
+ ├───┼───┤
254
+ │ 1 │ 2 │
255
+ ╰───┴───╯
256
+
257
+ Case 24 — No Leading Pipe on Separator
258
+
259
+ ╭───┬───╮
260
+ │ a │ b │
261
+ ├───┼───┤
262
+ │ 1 │ 2 │
263
+ ╰───┴───╯
264
+
265
+ Case 25 — No Leading Pipe on Body Row
266
+
267
+ ╭───┬───╮
268
+ │ a │ b │
269
+ ├───┼───┤
270
+ │ 1 │ 2 │
271
+ ╰───┴───╯
272
+
273
+ Case 26 — Indented Table Inside Ordered List
274
+
275
+ 1. Flatten it.
276
+
277
+ ╭─────────────┬───────────────┬───────╮
278
+ │ Service │ Sub │ Value │
279
+ ├─────────────┼───────────────┼───────┤
280
+ │ ollama_chat │ kramdown-ansi │ 🟢 │
281
+ ╰─────────────┴───────────────┴───────╯
282
+
283
+ Case 27 — Not a table just a pipe in a sentence
284
+
285
+ a | b
286
+
287
+ ────────────────────────────────────────────────────────────────────────────────
288
+
289
+ ╭──────┬────────────────────────────────────────────────────────────────╮
290
+ │ Case │ What It Stresses │
291
+ ├──────┼────────────────────────────────────────────────────────────────┤
292
+ │ 01 │ Column count validation / overflow / underflow │
293
+ ├──────┼────────────────────────────────────────────────────────────────┤
294
+ │ 02 │ Pipe-stripping logic (leading/trailing optional) │
295
+ ├──────┼────────────────────────────────────────────────────────────────┤
296
+ │ 03 │ Escape sequence handling (\|) │
297
+ ├──────┼────────────────────────────────────────────────────────────────┤
298
+ │ 04 │ Empty-string cell rendering, border alignment │
299
+ ├──────┼────────────────────────────────────────────────────────────────┤
300
+ │ 05 │ Single-column edge (degenerate table) │
301
+ ├──────┼────────────────────────────────────────────────────────────────┤
302
+ │ 06 │ Zero data rows — does it draw a header-only box? │
303
+ ├──────┼────────────────────────────────────────────────────────────────┤
304
+ │ 07 │ Consecutive tables — does the second become data of the first? │
305
+ ├──────┼────────────────────────────────────────────────────────────────┤
306
+ │ 08 │ Unicode width calculation for column sizing │
307
+ ├──────┼────────────────────────────────────────────────────────────────┤
308
+ │ 09 │ Inline markdown rendering inside a table cell │
309
+ ├──────┼────────────────────────────────────────────────────────────────┤
310
+ │ 10 │ Alignment row with fewer columns than header │
311
+ ├──────┼────────────────────────────────────────────────────────────────┤
312
+ │ 11 │ Blank line splitting one table into two │
313
+ ├──────┼────────────────────────────────────────────────────────────────┤
314
+ │ 12 │ Whitespace normalization / trimming │
315
+ ├──────┼────────────────────────────────────────────────────────────────┤
316
+ │ 13 │ Raw HTML + <br> in cell context │
317
+ ├──────┼────────────────────────────────────────────────────────────────┤
318
+ │ 14 │ Cell with only whitespace — empty or not? │
319
+ ├──────┼────────────────────────────────────────────────────────────────┤
320
+ │ 15 │ Very wide table — does it wrap or overflow? │
321
+ ├──────┼────────────────────────────────────────────────────────────────┤
322
+ │ 16 │ Non-standard alignment markers │
323
+ ├──────┼────────────────────────────────────────────────────────────────┤
324
+ │ 17 │ Table at doc start, glued to following header │
325
+ ├──────┼────────────────────────────────────────────────────────────────┤
326
+ │ 18 │ Table glued to preceding header (no blank before) │
327
+ ├──────┼────────────────────────────────────────────────────────────────┤
328
+ │ 19 │ Single col, missing closing pipe on header │
329
+ ├──────┼────────────────────────────────────────────────────────────────┤
330
+ │ 20 │ Not a table: single pipe line │
331
+ ├──────┼────────────────────────────────────────────────────────────────┤
332
+ │ 21 │ Not a table: header + sep, no body, missing pipe │
333
+ ├──────┼────────────────────────────────────────────────────────────────┤
334
+ │ 22 │ Table after code block, no blank between │
335
+ ├──────┼────────────────────────────────────────────────────────────────┤
336
+ │ 23 │ Table after list item, no blank between │
337
+ ├──────┼────────────────────────────────────────────────────────────────┤
338
+ │ 24 │ No leading pipe on separator row │
339
+ ├──────┼────────────────────────────────────────────────────────────────┤
340
+ │ 25 │ No leading pipe on body row │
341
+ ├──────┼────────────────────────────────────────────────────────────────┤
342
+ │ 26 │ Indented table inside ordered list item │
343
+ ├──────┼────────────────────────────────────────────────────────────────┤
344
+ │ 27 │ Not a table just a pipe in a sentence │
345
+ ╰──────┴────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,241 @@
1
+ | A | B |
2
+ |---|---|
3
+ | 1 | 2 |
4
+
5
+ ## Case 00 — Table at Document Start
6
+
7
+ ## Case 01 — Column Count Mismatch
8
+
9
+ Header has 3 columns, data rows have 5 and 1.
10
+
11
+ | A | B | C |
12
+ |:-:|:-:|:-:|
13
+ | 1 | 2 | 3 | 4 | 5 |
14
+ | only-one |
15
+ | a | b | c | d |
16
+
17
+ ## Case 02 — No Leading / Trailing Pipes
18
+
19
+ GFM allows omitting the outer `|`. Renderer must still split on inner pipes.
20
+
21
+ | A | B | C
22
+ |:-:|:-:|:-:
23
+ | 1 | 2 | 3
24
+ | 4 | 5 | 6
25
+
26
+ ## Case 03 — Escaped Pipe in Cell
27
+
28
+ `\|` should render as a literal `|`, not split the column.
29
+
30
+ | Col | Value |
31
+ |:----|:------|
32
+ | `a\|b` | 1 |
33
+ | `x \| y` | 2 |
34
+ | no-escape-but-literal | pipe |
35
+
36
+ ## Case 04 — Empty Cells
37
+
38
+ | A | B | C |
39
+ |:-:|:-:|:-:|
40
+ | | | |
41
+ | left | | right |
42
+ | | mid | |
43
+ | | | |
44
+
45
+ ## Case 05 — Single-Column Table
46
+
47
+ | Only |
48
+ |:-----|
49
+ | one |
50
+ | two |
51
+ | three |
52
+
53
+ ## Case 06 — Header + Alignment Only (No Data Rows)
54
+
55
+ not a table
56
+
57
+ | X | Y | Z |
58
+ |:-:|:-:|:-:|
59
+
60
+ ## Case 07 — Back-to-Back Tables (No Blank Line)
61
+
62
+ | T1a | T1b |
63
+ |:----|:----|
64
+ | a | b |
65
+ | T2a | T2b |
66
+ |:----|:----|
67
+ | c | d |
68
+
69
+ ## Case 08 — Full-Width / Variable-Width Unicode
70
+
71
+ CJK, emoji (double-width), combining marks — column widths will *lie*.
72
+
73
+ | Char | Width | Note |
74
+ |:-----|:------|:-----|
75
+ | `你` | 2 cells | CJK |
76
+ | `💜` | 2 cells | Emoji |
77
+ | `e\u0301` | 1+0 | Combining acute |
78
+ | `☝` | 2 cells | DBCS symbol |
79
+ | `|` | 1 cell | Looks like a pipe, isn't |
80
+
81
+ ## Case 09 — Markdown *Inside* Cells
82
+
83
+ Bold, italic, code, links, strikethrough, nested.
84
+
85
+ | Cell | Renders |
86
+ |:-----|:--------|
87
+ | `**bold**` | **bold** |
88
+ | `*it*` | *it* |
89
+ | `` `code` `` | `code` |
90
+ | `[link](https://x)` | [link](https://x) |
91
+ | `~~gone~~` | ~~gone~~ |
92
+ | `**_both_**` | **_both_** |
93
+
94
+ ## Case 10 — Alignment Row Has Wrong Column Count
95
+
96
+ | A | B | C |
97
+ |:-:|:-:|
98
+
99
+ ## Case 11 — Table Split by Blank Line
100
+
101
+ | A | B |
102
+ |:-:|:-:|
103
+ | 1 | 2 |
104
+
105
+ | A | B |
106
+ |:-:|:-:|
107
+ | 3 | 4 |
108
+
109
+ ## Case 12 — Excessive Whitespace Padding
110
+
111
+ | A | B | C |
112
+ | :---: | :---: | :---: |
113
+ | 1 | 2 | 3 |
114
+
115
+ ## Case 13 — `<br>` and HTML in Cells
116
+
117
+ | Cell | Content |
118
+ |:-----|:--------|
119
+ | br | line1<br>line2 |
120
+ | html | `<span>inline</span>` |
121
+ | both | **bold** <br> *italic* |
122
+
123
+ ## Case 14 — Pipe at Both Ends, Nothing Between
124
+
125
+ | |
126
+ |:-:|
127
+ | |
128
+
129
+ ## Case 15 — One Data Row, 10 Columns
130
+
131
+ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
132
+ |:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:--:|
133
+ | a | b | c | d | e | f | g | h | i | j |
134
+
135
+ ## Case 16 — Alignment Row with Mixed/Invalid Markers
136
+
137
+ | A | B | C |
138
+ |:--|--:|---|
139
+ | l | r | d |
140
+
141
+ ## Case 17 — Table at Document Start, Glued to Following Header
142
+
143
+ | a | b |
144
+ |---|---|
145
+ | 1 | 2 |
146
+ # next section
147
+
148
+ ## Case 18 — Table Glued to Preceding Header
149
+
150
+ # heading
151
+ | a | b |
152
+ |---|---|
153
+ | 1 | 2 |
154
+
155
+ ## Case 19 — Single Column, No Trailing Pipe on Header
156
+
157
+ | a table
158
+ |-|
159
+ |hi|
160
+
161
+ ## Case 20 — Not a Table: Single Pipe Line
162
+
163
+ |not a table
164
+
165
+ ## Case 21 — Not a Table: Header + Sep, No Body, Missing Closing Pipe
166
+
167
+ |not a table
168
+ |-|
169
+
170
+ ## Case 22 — Table After Code Block, Glued
171
+
172
+ ```
173
+ some code
174
+ ```
175
+ | a | b |
176
+ |---|---|
177
+ | 1 | 2 |
178
+
179
+ ## Case 23 — Table After List Item, Glued
180
+
181
+ - item one
182
+ - item two
183
+ | a | b |
184
+ |---|---|
185
+ | 1 | 2 |
186
+
187
+ ## Case 24 — No Leading Pipe on Separator
188
+
189
+ | a | b |
190
+ :---|---:
191
+ | 1 | 2 |
192
+
193
+ ## Case 25 — No Leading Pipe on Body Row
194
+
195
+ | a | b |
196
+ |---|---|
197
+ 1 | 2
198
+
199
+ ## Case 26 — Indented Table Inside Ordered List
200
+
201
+ 1. **Flatten it.**
202
+
203
+ | Service | Sub | Value |
204
+ |:--------|:----|:------|
205
+ | `ollama_chat` | `kramdown-ansi` | 🟢 |
206
+
207
+ ## Case 27 — Not a table just a pipe in a sentence
208
+
209
+ a | b
210
+
211
+ ---
212
+
213
+ | Case | What It Stresses |
214
+ |:-----|:-----------------|
215
+ | 01 | Column count validation / overflow / underflow |
216
+ | 02 | Pipe-stripping logic (leading/trailing optional) |
217
+ | 03 | Escape sequence handling (`\|`) |
218
+ | 04 | Empty-string cell rendering, border alignment |
219
+ | 05 | Single-column edge (degenerate table) |
220
+ | 06 | Zero data rows — does it draw a header-only box? |
221
+ | 07 | Consecutive tables — does the second become data of the first? |
222
+ | 08 | Unicode width calculation for column sizing |
223
+ | 09 | Inline markdown rendering *inside* a table cell |
224
+ | 10 | Alignment row with fewer columns than header |
225
+ | 11 | Blank line splitting one table into two |
226
+ | 12 | Whitespace normalization / trimming |
227
+ | 13 | Raw HTML + `<br>` in cell context |
228
+ | 14 | Cell with only whitespace — empty or not? |
229
+ | 15 | Very wide table — does it wrap or overflow? |
230
+ | 16 | Non-standard alignment markers |
231
+ | 17 | Table at doc start, glued to following header |
232
+ | 18 | Table glued to preceding header (no blank before) |
233
+ | 19 | Single col, missing closing pipe on header |
234
+ | 20 | Not a table: single pipe line |
235
+ | 21 | Not a table: header + sep, no body, missing pipe |
236
+ | 22 | Table after code block, no blank between |
237
+ | 23 | Table after list item, no blank between |
238
+ | 24 | No leading pipe on separator row |
239
+ | 25 | No leading pipe on body row |
240
+ | 26 | Indented table inside ordered list item |
241
+ | 27 | Not a table just a pipe in a sentence |
@@ -1,4 +1,8 @@
1
1
  describe Kramdown::ANSI::Width do
2
+ let :ansi do
3
+ Term::ANSIColor
4
+ end
5
+
2
6
  before do
3
7
  allow(Tins::Terminal).to receive(:columns).and_return 80
4
8
  end
@@ -58,12 +62,52 @@ describe Kramdown::ANSI::Width do
58
62
  expect(truncated.size).to eq 64
59
63
  end
60
64
 
65
+ it 'can truncate ansi escape sequences with length' do
66
+ truncated = described_class.truncate([ ansi.red { ?A * 10 }, ansi.green { ?A * 10 } ] * 5 * ' ', length: 64)
67
+ expect(truncated).to eq(
68
+ "\e[31mAAAAAAAAAA\e[0m \e[32mAAAAAAAAAA\e[0m \e[31mAAAAAAAAAA\e[0m \e[32mAAAAAAAAAA\e[0m \e[31mAAAAAAAAAA\e[0m \e[32mAAAAAAAA…\e[0m"
69
+ )
70
+ expect(Term::ANSIColor.uncolor {truncated }.size).to eq 64
71
+ end
72
+
73
+ it 'can truncate wide emojis with length' do
74
+ truncated = described_class.truncate('ABC' + ?😍 * 10 + 'DEF', length: 10)
75
+ expect(truncated).to eq("ABC😍😍😍…")
76
+ expect(truncated.size).to eq 7
77
+ expect(described_class.display_width(truncated)).to eq 10
78
+ truncated = described_class.truncate('AB' + ?😍 * 10 + 'DEF', length: 10)
79
+ expect(truncated).to eq("AB😍😍😍…")
80
+ expect(truncated.size).to eq 6
81
+ expect(described_class.display_width(truncated)).to eq 9
82
+ truncated = described_class.truncate('A' + ?😍 * 10 + 'DEF', length: 10)
83
+ expect(truncated).to eq("A😍😍😍😍…")
84
+ expect(truncated.size).to eq 6
85
+ expect(described_class.display_width(truncated)).to eq 10
86
+ truncated = described_class.truncate('A' * 5 + ?😍 + 'B' * 5, length: 10)
87
+ expect(truncated).to eq("AAAAA😍BB…")
88
+ expect(truncated.size).to eq 9
89
+ expect(described_class.display_width(truncated)).to eq 10
90
+ truncated = described_class.truncate('A' * 3 + ?😍 + 'B' + ?😁 + 'C' * 5, length: 10)
91
+ expect(truncated).to eq("AAA😍B😁C…")
92
+ expect(truncated.size).to eq 8
93
+ expect(described_class.display_width(truncated)).to eq 10
94
+ truncated = described_class.truncate('A' * 3 + ?😍 + ansi.red { 'B' } + ?😁 + 'C' * 5, length: 10)
95
+ expect(truncated).to eq("AAA😍\e[31mB\e[0m😁C…\e[0m")
96
+ expect(truncated.size).to eq 21
97
+ expect(described_class.display_width(truncated)).to eq 10
98
+ end
99
+
61
100
  it 'cannot truncate if not necessary' do
62
101
  text = [ ?A * 10 ] * 5 * ' '
63
102
  truncated = described_class.truncate(text, length: 54)
64
103
  expect(truncated).to eq text
65
104
  end
66
105
 
106
+ it 'can truncate empty strings' do
107
+ truncated = described_class.truncate('', length: 23)
108
+ expect(truncated).to be_empty
109
+ end
110
+
67
111
  it 'can truncate with length 0' do
68
112
  truncated = described_class.truncate([ ?A * 10 ] * 10 * ' ', length: 0)
69
113
  expect(truncated).to be_empty
@@ -76,5 +120,51 @@ describe Kramdown::ANSI::Width do
76
120
  )
77
121
  expect(truncated.size).to eq 64
78
122
  end
123
+
124
+ it 'keeps pure ANSI strings (no chars)' do
125
+ truncated = described_class.truncate(ansi.red { '' }, length: 10)
126
+ expect(truncated).to eq "\e[31m\e[0m"
127
+ expect(truncated.size).to eq 9
128
+ end
129
+
130
+ it 'handles zero-width characters correctly' do
131
+ src = "A#{("\u200d"*10)}B"
132
+ truncated = described_class.truncate(src, length: 3)
133
+ expect(described_class.display_width(truncated)).to eq(2)
134
+ expect(truncated).to match "A.*B"
135
+ end
136
+
137
+ it 'preserves newlines in multi-line text' do
138
+ truncated = described_class.truncate("hello\nworld", length: 8)
139
+ expect(truncated).to include("\n")
140
+ expect(described_class.display_width(truncated)).to eq 8
141
+ end
142
+
143
+ it 'supports multi‑cell ellipsis' do
144
+ truncated = described_class.truncate('ABC' + ?😇 * 10, length: 8, ellipsis: '… (more)')
145
+ expect(truncated).to eq "… (more)"
146
+ expect(described_class.display_width(truncated)).to eq 8
147
+ expect(truncated.length).to eq 8
148
+ end
149
+
150
+ it 'appends reset if truncated after an escape that lacks reset' do
151
+ truncated = described_class.truncate(ansi.red { 'Hello' }, length: 4)
152
+ expect(truncated).to eq "\e[31mHel…\e[0m"
153
+ expect(described_class.remove_ansi_escapes(truncated).size).to eq 4
154
+ end
155
+
156
+ it 'appends single reset for nested styles when truncated' do
157
+ truncated = described_class.truncate(ansi.red { ansi.bold { 'Hi' } }, length: 2)
158
+ expect(truncated).to eq "\e[31m\e[1mH…\e[0m"
159
+ expect(described_class.display_width(truncated)).to eq 2
160
+ end
161
+
162
+ it 'omits escapes that fall after the truncation point' do
163
+ src = "#{ansi.red { 'A' }}B#{ansi.green { 'C' }}D"
164
+ truncated = described_class.truncate(src, length: 2)
165
+ expect(truncated).not_to include "\e[32m"
166
+ expect(truncated).to eq "\e[31mA…\e[0m"
167
+ expect(described_class.display_width(truncated)).to eq 2
168
+ end
79
169
  end
80
170
  end