TUI: mouse drag selection with OSC 52 copy; Markdown emphasis follows CommonMark; v0.5.0
- 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:
Vendored
+115
-13
@@ -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++;
|
||||
|
||||
Reference in New Issue
Block a user