Support numbered lists in reflow-paragraph.
Add `reflow-paragraph` tests for numbered lists with hanging indents and extend support for numbered list parsing and wrapping logic.
This commit is contained in:
@@ -301,10 +301,15 @@ if (BUILD_TESTS)
|
|||||||
tests/test_buffer_io.cc
|
tests/test_buffer_io.cc
|
||||||
tests/test_piece_table.cc
|
tests/test_piece_table.cc
|
||||||
tests/test_search.cc
|
tests/test_search.cc
|
||||||
|
tests/test_reflow_paragraph.cc
|
||||||
|
|
||||||
# minimal engine sources required by Buffer
|
# minimal engine sources required by Buffer
|
||||||
PieceTable.cc
|
PieceTable.cc
|
||||||
Buffer.cc
|
Buffer.cc
|
||||||
|
Editor.cc
|
||||||
|
Command.cc
|
||||||
|
HelpText.cc
|
||||||
|
Swap.cc
|
||||||
OptimizedSearch.cc
|
OptimizedSearch.cc
|
||||||
UndoNode.cc
|
UndoNode.cc
|
||||||
UndoTree.cc
|
UndoTree.cc
|
||||||
|
|||||||
119
Command.cc
119
Command.cc
@@ -4026,6 +4026,29 @@ cmd_reflow_paragraph(CommandContext &ctx)
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
auto is_numbered_line = [&](const std::string &s,
|
||||||
|
std::string &indent_out,
|
||||||
|
std::string &marker_out,
|
||||||
|
std::size_t &after_prefix_idx) -> bool {
|
||||||
|
indent_out = leading_ws(s);
|
||||||
|
std::size_t i = indent_out.size();
|
||||||
|
if (i >= s.size() || !std::isdigit(static_cast<unsigned char>(s[i])))
|
||||||
|
return false;
|
||||||
|
std::size_t j = i;
|
||||||
|
while (j < s.size() && std::isdigit(static_cast<unsigned char>(s[j])))
|
||||||
|
++j;
|
||||||
|
if (j >= s.size())
|
||||||
|
return false;
|
||||||
|
char delim = s[j];
|
||||||
|
if (!(delim == '.' || delim == ')'))
|
||||||
|
return false;
|
||||||
|
if (j + 1 >= s.size() || s[j + 1] != ' ')
|
||||||
|
return false;
|
||||||
|
marker_out = s.substr(i, (j - i) + 1); // e.g. "1." or "10)"
|
||||||
|
after_prefix_idx = j + 2; // after delimiter + space
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
auto normalize_spaces = [](const std::string &in) {
|
auto normalize_spaces = [](const std::string &in) {
|
||||||
std::string out;
|
std::string out;
|
||||||
out.reserve(in.size());
|
out.reserve(in.size());
|
||||||
@@ -4107,20 +4130,25 @@ cmd_reflow_paragraph(CommandContext &ctx)
|
|||||||
|
|
||||||
std::vector<std::string> new_lines;
|
std::vector<std::string> new_lines;
|
||||||
|
|
||||||
// Determine if this region looks like a list: any line starting with bullet
|
// Determine if this region looks like a list: any line starting with bullet or number
|
||||||
bool region_has_bullet = false;
|
bool region_has_list = false;
|
||||||
for (std::size_t i = para_start; i <= para_end; ++i) {
|
for (std::size_t i = para_start; i <= para_end; ++i) {
|
||||||
std::string s = static_cast<std::string>(rows[i]);
|
std::string s = static_cast<std::string>(rows[i]);
|
||||||
std::string indent;
|
std::string indent;
|
||||||
char marker;
|
char marker;
|
||||||
std::size_t idx;
|
std::size_t idx;
|
||||||
if (is_bullet_line(s, indent, marker, idx)) {
|
if (is_bullet_line(s, indent, marker, idx)) {
|
||||||
region_has_bullet = true;
|
region_has_list = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
std::string nmarker;
|
||||||
|
if (is_numbered_line(s, indent, nmarker, idx)) {
|
||||||
|
region_has_list = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (region_has_bullet) {
|
if (region_has_list) {
|
||||||
// Parse as list items; support hanging indent continuations
|
// Parse as list items; support hanging indent continuations
|
||||||
for (std::size_t i = para_start; i <= para_end; ++i) {
|
for (std::size_t i = para_start; i <= para_end; ++i) {
|
||||||
std::string s = static_cast<std::string>(rows[i]);
|
std::string s = static_cast<std::string>(rows[i]);
|
||||||
@@ -4148,6 +4176,10 @@ cmd_reflow_paragraph(CommandContext &ctx)
|
|||||||
if (is_bullet_line(ns, nindent, nmarker, nidx)) {
|
if (is_bullet_line(ns, nindent, nmarker, nidx)) {
|
||||||
break; // next item
|
break; // next item
|
||||||
}
|
}
|
||||||
|
std::string nnmarker;
|
||||||
|
if (is_numbered_line(ns, nindent, nnmarker, nidx)) {
|
||||||
|
break; // next item
|
||||||
|
}
|
||||||
// Not a continuation and not a bullet: stop (treat as separate paragraph chunk)
|
// Not a continuation and not a bullet: stop (treat as separate paragraph chunk)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -4155,30 +4187,65 @@ cmd_reflow_paragraph(CommandContext &ctx)
|
|||||||
wrap_with_prefixes(content, first_prefix, cont_prefix, width, new_lines);
|
wrap_with_prefixes(content, first_prefix, cont_prefix, width, new_lines);
|
||||||
i = j - 1; // advance
|
i = j - 1; // advance
|
||||||
} else {
|
} else {
|
||||||
// A non-bullet line within a list region; treat as its own wrapped paragraph preserving its indent
|
std::string nmarker;
|
||||||
std::string base_indent = leading_ws(s);
|
if (is_numbered_line(s, indent, nmarker, after_idx)) {
|
||||||
std::string content = s.substr(base_indent.size());
|
std::string first_prefix = indent + nmarker + " ";
|
||||||
std::size_t j = i + 1;
|
std::string cont_prefix = indent + std::string(nmarker.size() + 1, ' ');
|
||||||
while (j <= para_end) {
|
std::string content = s.substr(after_idx);
|
||||||
std::string ns = static_cast<std::string>(rows[j]);
|
// consume continuation lines that are part of this numbered item
|
||||||
std::string nindent = leading_ws(ns);
|
std::size_t j = i + 1;
|
||||||
std::string tmp_indent;
|
while (j <= para_end) {
|
||||||
char tmp_marker;
|
std::string ns = static_cast<std::string>(rows[j]);
|
||||||
std::size_t tmp_idx;
|
if (starts_with(ns, cont_prefix)) {
|
||||||
if (is_bullet_line(ns, tmp_indent, tmp_marker, tmp_idx)) {
|
content += ' ';
|
||||||
break; // next bullet starts
|
content += ns.substr(cont_prefix.size());
|
||||||
}
|
++j;
|
||||||
if (nindent.size() >= base_indent.size()) {
|
continue;
|
||||||
content += ' ';
|
}
|
||||||
content += ns.substr(base_indent.size());
|
// stop if next item
|
||||||
++j;
|
std::string nindent2;
|
||||||
} else {
|
char bmarker;
|
||||||
|
std::size_t nidx;
|
||||||
|
if (is_bullet_line(ns, nindent2, bmarker, nidx))
|
||||||
|
break;
|
||||||
|
std::string nnmarker;
|
||||||
|
if (is_numbered_line(ns, nindent2, nnmarker, nidx))
|
||||||
|
break;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
content = normalize_spaces(content);
|
||||||
|
wrap_with_prefixes(content, first_prefix, cont_prefix, width, new_lines);
|
||||||
|
i = j - 1;
|
||||||
|
} else {
|
||||||
|
// A non-bullet line within a list region; treat as its own wrapped paragraph preserving its indent
|
||||||
|
std::string base_indent = leading_ws(s);
|
||||||
|
std::string content = s.substr(base_indent.size());
|
||||||
|
std::size_t j = i + 1;
|
||||||
|
while (j <= para_end) {
|
||||||
|
std::string ns = static_cast<std::string>(rows[j]);
|
||||||
|
std::string nindent = leading_ws(ns);
|
||||||
|
std::string tmp_indent;
|
||||||
|
char tmp_marker;
|
||||||
|
std::size_t tmp_idx;
|
||||||
|
if (is_bullet_line(ns, tmp_indent, tmp_marker, tmp_idx)) {
|
||||||
|
break; // next bullet starts
|
||||||
|
}
|
||||||
|
std::string tmp_nmarker;
|
||||||
|
if (is_numbered_line(ns, tmp_indent, tmp_nmarker, tmp_idx)) {
|
||||||
|
break; // next numbered starts
|
||||||
|
}
|
||||||
|
if (nindent.size() >= base_indent.size()) {
|
||||||
|
content += ' ';
|
||||||
|
content += ns.substr(base_indent.size());
|
||||||
|
++j;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content = normalize_spaces(content);
|
||||||
|
wrap_with_prefixes(content, base_indent, base_indent, width, new_lines);
|
||||||
|
i = j - 1;
|
||||||
}
|
}
|
||||||
content = normalize_spaces(content);
|
|
||||||
wrap_with_prefixes(content, base_indent, base_indent, width, new_lines);
|
|
||||||
i = j - 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -4566,4 +4633,4 @@ Execute(Editor &ed, const std::string &name, const std::string &arg, int count)
|
|||||||
return false;
|
return false;
|
||||||
CommandContext ctx{ed, arg, count};
|
CommandContext ctx{ed, arg, count};
|
||||||
return cmd->handler ? cmd->handler(ctx) : false;
|
return cmd->handler ? cmd->handler(ctx) : false;
|
||||||
}
|
}
|
||||||
102
tests/test_reflow_paragraph.cc
Normal file
102
tests/test_reflow_paragraph.cc
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
#include "Test.h"
|
||||||
|
|
||||||
|
#include "Buffer.h"
|
||||||
|
#include "Command.h"
|
||||||
|
#include "Editor.h"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
|
||||||
|
static std::string
|
||||||
|
to_string_rows(const Buffer &buf)
|
||||||
|
{
|
||||||
|
std::string out;
|
||||||
|
for (const auto &r: buf.Rows()) {
|
||||||
|
out += static_cast<std::string>(r);
|
||||||
|
out.push_back('\n');
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (ReflowParagraph_NumberedList_HangingIndent)
|
||||||
|
{
|
||||||
|
InstallDefaultCommands();
|
||||||
|
|
||||||
|
Editor ed;
|
||||||
|
ed.SetDimensions(24, 80);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
// Two list items in one paragraph (no blank lines).
|
||||||
|
// Second line of each item already uses a hanging indent.
|
||||||
|
const std::string initial =
|
||||||
|
"1. one two three four five six seven eight nine ten eleven\n"
|
||||||
|
" twelve thirteen fourteen\n"
|
||||||
|
"10. alpha beta gamma delta epsilon zeta eta theta iota kappa lambda\n"
|
||||||
|
" mu nu xi omicron\n";
|
||||||
|
b.insert_text(0, 0, initial);
|
||||||
|
// Put cursor on first item
|
||||||
|
b.SetCursor(0, 0);
|
||||||
|
ed.AddBuffer(std::move(b));
|
||||||
|
|
||||||
|
Buffer *buf = ed.CurrentBuffer();
|
||||||
|
ASSERT_TRUE(buf != nullptr);
|
||||||
|
|
||||||
|
const int width = 25;
|
||||||
|
ASSERT_TRUE(Execute(ed, std::string("reflow-paragraph"), std::string(), width));
|
||||||
|
|
||||||
|
const auto &rows = buf->Rows();
|
||||||
|
ASSERT_TRUE(!rows.empty());
|
||||||
|
const std::string dump = to_string_rows(*buf);
|
||||||
|
|
||||||
|
// Find the start of the second item.
|
||||||
|
bool any_too_long = false;
|
||||||
|
std::size_t idx_10 = rows.size();
|
||||||
|
for (std::size_t i = 0; i < rows.size(); ++i) {
|
||||||
|
const std::string line = static_cast<std::string>(rows[i]);
|
||||||
|
if (static_cast<int>(line.size()) > width)
|
||||||
|
any_too_long = true;
|
||||||
|
if (line.rfind("10. ", 0) == 0) {
|
||||||
|
idx_10 = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ASSERT_TRUE(idx_10 < rows.size());
|
||||||
|
if (any_too_long) {
|
||||||
|
std::cerr << "Reflow produced a line longer than width=" << width << "\n";
|
||||||
|
std::cerr << to_string_rows(*buf) << "\n";
|
||||||
|
}
|
||||||
|
EXPECT_TRUE(!any_too_long);
|
||||||
|
|
||||||
|
// Item 1: first line has "1. ", continuation lines have 3 spaces.
|
||||||
|
for (std::size_t i = 0; i < idx_10; ++i) {
|
||||||
|
const std::string line = static_cast<std::string>(rows[i]);
|
||||||
|
if (i == 0) {
|
||||||
|
ASSERT_TRUE(line.rfind("1. ", 0) == 0);
|
||||||
|
} else {
|
||||||
|
ASSERT_TRUE(line.rfind(" ", 0) == 0);
|
||||||
|
ASSERT_TRUE(line.rfind("1. ", 0) != 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item 10: first line has "10. ", continuation lines have 4 spaces.
|
||||||
|
ASSERT_TRUE(static_cast<std::string>(rows[idx_10]).rfind("10. ", 0) == 0);
|
||||||
|
bool bad_10 = false;
|
||||||
|
for (std::size_t i = idx_10 + 1; i < rows.size(); ++i) {
|
||||||
|
const std::string line = static_cast<std::string>(rows[i]);
|
||||||
|
if (line.empty())
|
||||||
|
break; // paragraph terminator / trailing empty line
|
||||||
|
if (line.rfind(" ", 0) != 0)
|
||||||
|
bad_10 = true;
|
||||||
|
if (line.rfind("10. ", 0) == 0)
|
||||||
|
bad_10 = true;
|
||||||
|
}
|
||||||
|
if (bad_10) {
|
||||||
|
std::cerr << "Unexpected prefix in reflow output:\n" << dump << "\n";
|
||||||
|
}
|
||||||
|
ASSERT_TRUE(!bad_10);
|
||||||
|
|
||||||
|
// Debug helper if something goes wrong (kept as a string for easy inspection).
|
||||||
|
EXPECT_TRUE(!to_string_rows(*buf).empty());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user