TUI: mouse drag selection with OSC 52 copy; Markdown emphasis follows CommonMark; v0.5.0
ci / linux (clang) (push) Failing after 21m53s
ci / linux (gcc) (push) Failing after 48s
ci / musl (push) Canceled after 0s
ci / macos (push) Canceled after 0s
ci / sanitizers (push) Canceled after 11m39s

- Drag, double-click (word) and triple-click (line) selection in the log,
  auto-scroll at the edges, soft-wrapped lines joined on copy.
- sashidown ANSI renderer: delimiter runs with flanking rules, so
  intraword underscores (test_apply_patch.c) stay literal; unmatched
  * _ and backticks are shown as-is; * and _ do not close each other.
- tests: test_textsel, test_markdown.
This commit is contained in:
WHC
2026-10-06 14:24:42 +08:00
parent b28bca2cfc
commit 1c26cc6e30
11 changed files with 875 additions and 111 deletions
+115 -13
View File
@@ -12,8 +12,88 @@ typedef struct {
uint8_t code;
uint8_t latex;
uint8_t strike;
char bold_ch; /* '*' or '_' that opened bold / italic */
char italic_ch;
} ansi_inline_t;
/* --- emphasis delimiters (CommonMark flanking rules, simplified) ---------- */
/* Neighbour of a delimiter run: whitespace (also line start/end), ASCII
* punctuation, or anything else. Non-ASCII counts as a letter so that CJK
* text and its full-width punctuation behave like words. */
enum { MD_SPACE, MD_PUNCT, MD_OTHER };
static int md_class(unsigned char c)
{
if (c == ' ' || c == '\t' || c == '\n' || c == '\r')
return MD_SPACE;
if ((c >= 0x21 && c <= 0x2F) || (c >= 0x3A && c <= 0x40) ||
(c >= 0x5B && c <= 0x60) || (c >= 0x7B && c <= 0x7E))
return MD_PUNCT;
return MD_OTHER;
}
static size_t md_run(const char *input, size_t end, size_t i)
{
size_t j = i;
while (j < end && input[j] == input[i])
j++;
return j - i;
}
/* Flanking of the run [i, i+n) within [start, end). */
static void md_flank(const char *input, size_t start, size_t end, size_t i,
size_t n, int *left, int *right, int *pc, int *nc)
{
*pc = md_class(i > start ? (unsigned char)input[i - 1] : ' ');
*nc = md_class(i + n < end ? (unsigned char)input[i + n] : ' ');
*left = *nc != MD_SPACE && (*nc != MD_PUNCT || *pc != MD_OTHER);
*right = *pc != MD_SPACE && (*pc != MD_PUNCT || *nc != MD_OTHER);
}
/* '_' must not open or close inside a word (snake_case, __init__). */
static int md_can_open(const char *input, size_t start, size_t end, size_t i,
size_t n)
{
int l, r, pc, nc;
md_flank(input, start, end, i, n, &l, &r, &pc, &nc);
return input[i] == '*' ? l : l && (!r || pc == MD_PUNCT);
}
static int md_can_close(const char *input, size_t start, size_t end, size_t i,
size_t n)
{
int l, r, pc, nc;
md_flank(input, start, end, i, n, &l, &r, &pc, &nc);
return input[i] == '*' ? r : r && (!l || nc == MD_PUNCT);
}
/* Whether a run of n delimiters like input[i] that can close follows on this
* line, outside code spans. */
static int md_has_closer(const char *input, size_t start, size_t end,
size_t i, size_t n)
{
char d = input[i];
size_t k = i + n;
while (k < end) {
char c = input[k];
if (c == '\\' && k + 1 < end) {
k += 2;
} else if (c == '`') {
size_t j = block_scan_until_byte(input, end, k + 1, '`');
k = j < end ? j + 1 : k + 1;
} else if (c == d) {
size_t m = md_run(input, end, k);
if (m == n && md_can_close(input, start, end, k, m))
return 1;
k += m;
} else {
k++;
}
}
return 0;
}
typedef struct {
uint8_t ul;
uint8_t ol;
@@ -100,7 +180,7 @@ static void ansi_render_inline(sashidown_ctx_t *ctx, const ansi_theme_t *th,
const char *input, size_t len,
size_t start, size_t end)
{
ansi_inline_t st = {0, 0, 0, 0, 0};
ansi_inline_t st = {0, 0, 0, 0, 0, 0, 0};
size_t i = start;
while (i < end) {
@@ -194,10 +274,39 @@ static void ansi_render_inline(sashidown_ctx_t *ctx, const ansi_theme_t *th,
}
}
if ((ch == '*' || ch == '_') && i + 1 < end && input[i + 1] == (char)ch) {
ansi_s(ctx, st.bold ? th->strong_off : th->strong_on);
st.bold = !st.bold;
i += 2;
if (ch == '*' || ch == '_') {
/* run of 1 = italic, 2 = bold, 3 = both; others are literal */
size_t n = md_run(input, end, i);
int b = n >= 2 && n <= 3, it = n == 1 || n == 3;
int open_b = st.bold && st.bold_ch == (char)ch;
int open_i = st.italic && st.italic_ch == (char)ch;
if (n <= 3 && (!b || open_b) && (!it || open_i) &&
md_can_close(input, start, end, i, n)) {
if (it) {
ansi_s(ctx, th->emph_off);
st.italic = 0;
}
if (b) {
ansi_s(ctx, th->strong_off);
st.bold = 0;
}
} else if (n <= 3 && (!b || !st.bold) && (!it || !st.italic) &&
md_can_open(input, start, end, i, n) &&
md_has_closer(input, start, end, i, n)) {
if (b) {
ansi_s(ctx, th->strong_on);
st.bold = 1;
st.bold_ch = (char)ch;
}
if (it) {
ansi_s(ctx, th->emph_on);
st.italic = 1;
st.italic_ch = (char)ch;
}
} else {
fsm_append_len(ctx, input + i, n);
}
i += n;
continue;
}
@@ -208,14 +317,7 @@ static void ansi_render_inline(sashidown_ctx_t *ctx, const ansi_theme_t *th,
continue;
}
if (ch == '*' || ch == '_') {
ansi_s(ctx, st.italic ? th->emph_off : th->emph_on);
st.italic = !st.italic;
i++;
continue;
}
if (ch == '`') {
if (ch == '`' && block_scan_until_byte(input, end, i + 1, '`') < end) {
ansi_s(ctx, th->code_on);
st.code = 1;
i++;