19 KiB
介绍
txls是varch的文本表格,语法参考markdown表格的语法,为了更好的文本直观性,txls语法规则比markdown表格语法稍微严格一点。通过txls,可以很方便的访问一个txls文件的行列内容,以及生成一个整齐规范可读性高的文本形式的表格文件。
简单介绍下txls规范,以及对比下markdown表格的语法。
表头
表头和markdown表格表头很类似,只不过稍微严格点
markdown表格不要求当前行两端一定需要`|`分隔符,但是txls为了更整齐的格式,要求行的两端必须包含`|`分割符。后面的每一行内容也有如此要求。
表头后面跟随分割行,分割行的列数需与表头列数一致
分割行的内容必须包含连续的`-`
例子:
| col 1 | col 2 | col 3 | col 4 | col 5 |
|-------|-------|--------------------|-------|-------|
行
在一行中,以`|`分隔符作为区分列
在`|`分隔符之间,包含的即为单元格内容,单元格内容不包含两端的空格部分
单元格内容内容可以包换转义字符 "\|" 表示 '|',"<br>" 表示 '\n'
例子:
| col 1 | Zhang san | col 3 | col 4 | col 5 |
|-------|----------:|----------------|-------|-------|
| 11 | 21 | 31 | 41 | 51 |
| 12 | 22 | 1234\|<br>5678 | 42 | 52 |
| 13 | 23 | 33 | 43 | 53 |
| 14 | 24 | 34 | 44 | 54 |
| 15 | 25 | 35 | 45 | 55 |
对齐方式
txls的对齐方式一共三种,左对齐、右对齐、居中对齐。在表格中的标识为':',在表格分割行对应列单元格两端位置。
':'在左为左对齐,在右为右对齐,左右都有则为居中对齐,都没有则为默认对齐(目前txls默认对齐为左对齐)
':'在两端的位置最多仅能为1个
例子:
| left align | right align | center align | default align |
|:-----------------|-----------------:|:----------------:|------------------|
| 0 | 0 | 0 | 0 |
| 0123456789abcdef | 0123456789abcdef | 0123456789abcdef | 0123456789abcdef |
接口
创建和删除txls对象
txls_t txls_create(int col, int row);
void txls_delete(txls_t txls);
其中txls_t为txls的结构体,创建方法会创建一个col列row行的表格,创建成功则返回txls对象的句柄,删除方法则是删除一个txls对象
获取行列数
int txls_col(txls_t txls);
int txls_row(txls_t txls);
这两个方法分别返回txls表格的列数和行数
插入和删除一列
int txls_insert_column(txls_t txls, int col);
int txls_delete_column(txls_t txls, int col);
这两个方法分别向txls插入和删除一列,列的索引从1开始,成功返回1失败返回0
插入和删除一行
int txls_insert_row(txls_t txls, int row);
int txls_delete_row(txls_t txls, int row);
这两个方法分别向txls插入和删除一行,行的索引从1开始,成功返回1失败返回0
设置和获取单元格内容
int txls_set_text(txls_t txls, int col, int row, const char* text);
const char* txls_get_text(txls_t txls, int col, int row);
设置方法,就是将txls指定的单元格内容设定为指定的text,文本内容的两端不建议设为空格,在写入表格后将会忽略,同时也不能带有换行符除外的不可视字符。成功返回1,失败返回0。
获取方法则是返回指定单元的内容,返回空即代表获取失败。
当row传入0时候,则是相应设置和获取表头。
#define txls_set_head(txls, col, head)
#define txls_get_head(txls, col)
设置对齐方式
int txls_set_align(txls_t txls, int col, int align);
设置指定列的对齐方式,方式包括有TXLS_ALIGN_LEFT,TXLS_ALIGN_RIGHT,TXLS_ALIGN_CENTER三种,其他的均为未知对齐方式。
txls对象转储
char* txls_dumps(txls_t txls, int neat, int* len);
int txls_file_dump(txls_t txls, const char* filename);
首先txls_dumps方法,将txls对象按格式转储为字符串。其中传入的neat参数,是否整齐的转储的控制变量,0为不整齐输出,其他为整齐输出。整齐输出,就是每一列都是对齐同宽度的输出,不整齐输出则是单元格实际多长就输出多长,整齐输出保持美观可读性,但是会占用比较多空间来存放空格。len则是转换出来的字符串长度,传入NULL时候就是不获取长度。返回值则是转换出来的字符串,这个字符串是函数分配的,在结束使用需要free掉。
txls_file_dump方法则是在txls_dumps的基础上将txls转储到文件当中,filename传入文件名,返回值为转储的长度,负值表示转储失败。
txls对象加载
txls_t txls_loads(const char* text);
txls_t txls_file_load(const char* filename);
类似转储的方法,txls对象可以从字符串文本中加载,也可以从文件中加载。加载成功则会返回一个txls对象,失败则返回NULL。
txls加载错误
int txls_error_info(int* line);
varch的txls解析器提供了精准的错误识别,在txls加载的失败的时候可以调用此方法去定位错误位置和错误的类型。返回值为1表示当前解析出错了,0则为未出错。line输出错误所在行,type输出错误的类型。错误类型定义如下:
#define TXLS_E_OK (0) /* no error */
#define TXLS_E_HEAD (1) /* irregular header format */
#define TXLS_E_ALLOC (2) /* failed to allocate space */
#define TXLS_E_BEGIN (3) /* missing "|" separator at the begin */
#define TXLS_E_END (4) /* missing "|" separator at the end */
#define TXLS_E_IDENT (5) /* missing "-" separator at split row */
#define TXLS_E_BRANK (6) /* there are extra blank lines */
#define TXLS_E_MEMORY (7) // memory allocation failed
#define TXLS_E_OPEN (8) // fail to open file
参考例子
生成txls文件
void test(void)
{
txls_t x = NULL; // 定义txls对象,习惯初始化为NULL
x = txls_create(4, 5); // 创建4x5的表格
if (!x)
{
return;
}
/* 设置表头,第一列留空 */
txls_set_head(x, 2, "Zhang San");
txls_set_head(x, 3, "Li Si");
txls_set_head(x, 4, "Wang Wu");
/* 设置对齐方式 */
txls_set_align(x, 2, TXLS_ALIGN_LEFT);
txls_set_align(x, 3, TXLS_ALIGN_CENTER);
txls_set_align(x, 4, TXLS_ALIGN_RIGHT);
/* 第一列作为信息类别 */
txls_set_text(x, 1, 1, "age");
txls_set_text(x, 1, 2, "gender");
txls_set_text(x, 1, 3, "height");
txls_set_text(x, 1, 4, "weight");
txls_set_text(x, 1, 5, "email");
/* 写入每个人信息 */
// Zhang San
txls_set_text(x, 2, 1, "18");
txls_set_text(x, 2, 2, "man");
txls_set_text(x, 2, 3, "178.5");
txls_set_text(x, 2, 4, "65");
txls_set_text(x, 2, 5, "123321@qq.com");
// Li Si
txls_set_text(x, 3, 1, "24");
txls_set_text(x, 3, 2, "woman");
txls_set_text(x, 3, 3, "165");
txls_set_text(x, 3, 4, "48");
txls_set_text(x, 3, 5, "lisi@163.com");
// Wang Wu
txls_set_text(x, 4, 1, "20");
txls_set_text(x, 4, 2, "man");
txls_set_text(x, 4, 3, "175");
txls_set_text(x, 4, 4, "75");
txls_set_text(x, 4, 5, "ww1234567890@qq.com");
txls_file_dump(x, "info.txls");
txls_delete(x);
}
转储的文件 info.txls
| | Zhang San | Li Si | Wang Wu |
|--------|:--------------|:------------:|--------------------:|
| age | 18 | 24 | 20 |
| gender | man | woman | man |
| height | 178.5 | 165 | 175 |
| weight | 65 | 48 | 75 |
| email | 123321@qq.com | lisi@163.com | ww1234567890@qq.com |
通过markdown阅读器的显示效果
| Zhang San | Li Si | Wang Wu | |
|---|---|---|---|
| age | 18 | 24 | 20 |
| gender | man | woman | man |
| height | 178.5 | 165 | 175 |
| weight | 65 | 48 | 75 |
| 123321@qq.com | lisi@163.com | ww1234567890@qq.com |
例子里面使用函数很多没有对返回值进行判断,实际应用需对返回值进行判断。
加载txls文件
在上面生成的文件的基础上,进行加载该文件。
加载测试代码
void test(void)
{
txls_t x = NULL; // 定义txls对象,习惯初始化为NULL
/* 加载txls文件 */
x = txls_file_load("info.txls");
if (!x) // 加载失败,定位错误
{
int line, type;
type = txls_error_info(&line);
printf("txls parse error! line %d, error %d.\r\n", line, type);
return;
}
/* 遍历表头,定位所在列 */
int col = 0;
for (col = 1; col <= txls_col(x); col++)
{
if (strcmp("Li Si", txls_get_head(x, col)) == 0)
{
break;
}
}
if (col > txls_col(x)) // 没有查找到
{
printf("Lookup failed\r\n");
return;
}
/* 打印信息 */
printf("name: %s, age=%s, gender: %s, height=%s, weight=%s, email:%s\r\n",
txls_get_text(x, col, 0),
txls_get_text(x, col, 1),
txls_get_text(x, col, 2),
txls_get_text(x, col, 3),
txls_get_text(x, col, 4),
txls_get_text(x, col, 5));
txls_delete(x); // 用完之后需要删除
}
运行结果:
name: Li Si, age=24, gender: woman, height=165, weight=48, email:lisi@163.com
加载错误
在上面例子的基础上把文件修改一下,把在第4行行末的分割符'|'删掉再加载。
| | Zhang San | Li Si | Wang Wu |
|--------|:--------------|:------------:|--------------------:|
| age | 18 | 24 | 20 |
| gender | man | woman | man
| height | 178.5 | 165 | 175 |
| weight | 65 | 48 | 75 |
| email | 123321@qq.com | lisi@163.com | ww1234567890@qq.com |
运行结果:
txls parse error! line 4, error 4.
如此能定位到4行出现4号错误,也就是
#define TXLS_E_END (4) /* missing "|" separator at the end */
源码解析
txls解析器结构体
txls解析器的所有结构体都是隐式的,也就是不能直接访问到结构体成员的,这样子的方式保证了模块的独立与安全,防止外部调用修改结构体的成员导致txls存储结构的破坏。所以txls解析器只留了唯一一个txls的声明在头文件,然后结构体的定义都在源文件。只能使用txls解析器提供的方法对txls对象进行操作。
txls类型声明
typedef struct TXLS *txls_t;
使用时候,只是用txls_t即可。
/* type of txls */
typedef struct TXLS
{
COLUMN *columns; /* columns base */
ITERATOR iterator; /* column iterator */
int col; /* column count */
int row; /* row count */
} TXLS;
TXLS结构体中包含了4个成员,columns(columns的链表),iterator(columns的迭代器),col和row就分别是表格的列和行数。特别说明这个iterator,就是这个迭代器记录列访问时候的位置,当再一次访问检查到是同位置时候,就可以快速返回,而不用从头到尾再遍历。迭代器后续再说明。
/* column storage */
typedef struct COLUMN
{
struct COLUMN *next; /* next column */
CELL *cells; /* the cell base address of this column */
ITERATOR iterator; /* cell list iterator */
int align; /* alignment */
int width; /* the output width of this column when neatly outputting */
} COLUMN;
看COLUMN结构体,包含了5个成员,next(指向下一个COLUMN,形成单向链表),cells(单元格链表),iterator(pair的迭代器),align(对齐方式),width(整齐输出时候的列的宽度)。
/* smallest memory cell */
typedef struct CELL
{
struct CELL *next; /* next cell */
char* address; /* address of cell content */
} CELL;
再看CELL结构体,包含2个成员,next(指向下一个CELL,形成单向链表),address(单元格内容,字符串)。
typedef struct
{
void *p; /* iteration pointer */
int i; /* iteration index */
} ITERATOR;
最后看看这个迭代器,其实这个迭代器很简单,就是记录当前所指向的单向链表的结点(成员p),以及记录当前所在单向链表的索引。具体是怎么记录的下文再聊。
结构体介绍到这里,TXLS类的存储结构已经很明了,首先一个链表串起来每一列,每一列下面又串一个链表串起一列的单元格。
INI <it>
col[1] --> col[2] --> col[3] --> col[4]
| | | |
V V V V
cell[0] cell[0] cell[0] cell[0]
| | | |
V V V V
cell[1] cell[1] cell[1] cell[1]
| | | |
V V V V
cell[2] cell[2] cell[2] cell[2]
单向链表的迭代
单向链表的操作不是这里的重点,这里内置的迭代器为了提高单向链表的访问效率,迭代过程着重说明下。以column链表迭代为说明,说明下这个迭代器获取的链表结点的过程:
static COLUMN *txls_column(txls_t txls, int index, int col) // 传入索引和限定的列数
{
if (index >= col) return NULL; // 判断索引有没有越界
/*
这个一步是重置迭代,也就是将迭代器定位回到链表首位
满足其中3个条件之一都重置迭代器
1、因为单向链表,不能反向指向,所以目标索引小于迭代器的索引时候,就要重置,然后从链表首位迭代到指定索引
2、迭代器指针(p成员)为空时,为空则没有指向具体的结点,当然得重置迭代,所以外部想重置迭代器,只需将p成员置NULL即可
3、目标索引index为0时,主动获取第0位,也就是首位
*/
if (index < txls->iterator.i || !txls->iterator.p || index == 0)
{
txls->iterator.i = 0;
txls->iterator.p = txls->columns;
}
/*
循环将迭代器迭代到指定的索引位置
单向链表索引正向递增,所以正向遍历时候时间复杂度O(n),反向遍历还是O(n^2)
*/
while (txls->iterator.p && txls->iterator.i < index)
{
txls->iterator.p = ((COLUMN *)(txls->iterator.p))->next;
txls->iterator.i++;
}
/* 返回迭代器指向的结点 */
return txls->iterator.p;
}
迭代器在对链表进行调整的时候需要相应调整,最简单的方式就是把成员p设为NULL重置迭代器。
txls转储说明
转储就是按格式将txls“打印”出来,不过的是,不是打印在控制台,而是打印在指定内存空间。那这个空间哪来的呢?是动态内存分配来的,分配多少呢?如果是整齐输出的(neat不为0),那么就可以根据列宽预测到需要多少的空间,不整齐输出则需要在转储时候动态调整大小。
现在先来看维护这个打印空间的结构体:
typedef struct
{
char* address; /* buffer base address */
int size; /* size of buffer */
int end; /* end of buffer used */
} BUFFER;
一共也就是3个成员,address(空间的基地址),size(空间的大小),end(已使用的结尾,索引)。
动态调整空间的过程:
static int buf_append(BUFFER *buf, int needed) // neede为所需的追加的容量
{
char* address;
int size;
if (!buf || !buf->address) return 0;
/* 计算当前已使用的加上所需的一共所需多大的空间 */
needed += buf->end;
/* 计算当前的空间还能满足需要 */
if (needed <= buf->size) return 1; /* there is still enough space in the current buf */
/*
当前空间大小满足不了所需,重新计算能满足所需的空间
新的空间不是满足当前所需就行了的,还得留有余量,让下次追加时候用
不然每次追加一点容量都要重新分配空间,很浪费效率
而新分配的空间要多大才好,2的平方,也就是比所需大的最小的2的次方
为什么选择2的次方?
1、算法好实现,计算比指定值大的最小2的次方数好实现
2、空间利用率好,为 (1 + 2)/ 2 = 75%
3、空间重分配的次数小,比如最终需要128时候,每次定量10的空间新增需要13次,而2的次方只需7次。
*/
size = pow2gt(needed);
address = (char*)realloc(buf->address, size);
if (!address) return 0;
buf->size = size;
buf->address = address;
return 1;
}
以其中一段转储的代码为例,看看这个是怎么使用的
if (!buf_append(buf, 2)) return 0; // 先扩展空间
buf_push(buf, '|'); // 调用函数把字符push到buf里面
buf_push(buf, '\n');
在转储cell的时候有个点需要注意的,就是cell是允许换行和分隔符'|'的,换行符用<br>来代替,分隔符'|'则转移\|来代替。
while (addr && *addr)
{
if (*addr == '\n')
{
buf_push(buf, '<');
buf_push(buf, 'b');
buf_push(buf, 'r');
buf_push(buf, '>');
}
else if (*addr == '|')
{
buf_push(buf, '\\');
buf_push(buf, '|');
}
else
{
buf_push(buf, *addr);
}
addr++;
}
最终打印时候就是将txls遍历,动态调整空间把每个每一列每一个单元格按顺序打印到指定空间。
txls加载说明
txls解析器最重要以及最复杂的部分就这个加载解析的部分。
-
1、首先创建一个
0x0txls表格对象,然后在随着解析把解析到的表头、行、列这些逐个添加到txls对象,等到解析完就可以得到一个完整的txls对象 -
2、在解析之前的首先得将解析的行号以及错误重置,然后随着解析遇到换行符就让行号相应的递增,碰到解析出错了,就记录下错误的类型
-
3、然后就到了真正的解析过程,解析分为两步走,第一步先解析表头和表格分割行,这一步是确认这个表格一共有多少列,对齐方式,以及语法符不符合表头语法。第二步,就在确定的列的基础上,逐行的解析,解析每一行所区分的单元格并把单元格内容归类到指定行列里面。
整个解析过程如下
/* 创建一个空表格 */
txls = txls_create(0, 0);
...
/* 重置错误信息 */
etype = TXLS_E_OK;
eline = 1;
/* 解析表头 */
s = parse_head(text, txls);
if (etype) goto FAIL;
while (1)
{
/* 解析每一行 */
s = parse_line(s, txls);
if (etype) goto FAIL;
if (!*s) break;
}
return txls;
因为txls语法规定的内容基本都是以行来划分(value特殊情况可以换行),所以基本在这个循环里面就是在处理一行行的信息。
txls的增删改查说明
剩下的针对txls增删改查的其实已经没什么特别说明的了,就是对链表数据结构的基本操作,这里不着重说明了