mirror of
https://gitee.com/Lamdonn/varch.git
synced 2025-12-06 08:46:42 +08:00
507 lines
19 KiB
Markdown
507 lines
19 KiB
Markdown
## 介绍
|
||
|
||
txls是varch的文本表格,语法参考markdown表格的语法,为了更好的文本直观性,txls语法规则比markdown表格语法稍微严格一点。通过txls,可以很方便的访问一个txls文件的行列内容,以及生成一个整齐规范可读性高的文本形式的表格文件。
|
||
|
||
简单介绍下txls规范,以及对比下markdown表格的语法。
|
||
|
||
**表头**
|
||
```
|
||
表头和markdown表格表头很类似,只不过稍微严格点
|
||
markdown表格不要求当前行两端一定需要`|`分隔符,但是txls为了更整齐的格式,要求行的两端必须包含`|`分割符。后面的每一行内容也有如此要求。
|
||
表头后面跟随分割行,分割行的列数需与表头列数一致
|
||
分割行的内容必须包含连续的`-`
|
||
```
|
||
例子:
|
||
```md
|
||
| 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对象
|
||
```c
|
||
txls_t txls_create(int col, int row);
|
||
void txls_delete(txls_t txls);
|
||
```
|
||
其中**txls_t**为txls的结构体,创建方法会创建一个col列row行的表格,创建成功则返回txls对象的句柄,删除方法则是删除一个txls对象
|
||
|
||
### 获取行列数
|
||
```c
|
||
int txls_col(txls_t txls);
|
||
int txls_row(txls_t txls);
|
||
```
|
||
这两个方法分别返回txls表格的列数和行数
|
||
|
||
### 插入和删除一列
|
||
```c
|
||
int txls_insert_column(txls_t txls, int col);
|
||
int txls_delete_column(txls_t txls, int col);
|
||
```
|
||
这两个方法分别向txls插入和删除一列,列的索引从1开始,成功返回1失败返回0
|
||
|
||
### 插入和删除一行
|
||
```c
|
||
int txls_insert_row(txls_t txls, int row);
|
||
int txls_delete_row(txls_t txls, int row);
|
||
```
|
||
这两个方法分别向txls插入和删除一行,行的索引从1开始,成功返回1失败返回0
|
||
|
||
### 设置和获取单元格内容
|
||
```c
|
||
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时候,则是相应设置和获取表头。
|
||
```c
|
||
#define txls_set_head(txls, col, head)
|
||
#define txls_get_head(txls, col)
|
||
```
|
||
|
||
### 设置对齐方式
|
||
```c
|
||
int txls_set_align(txls_t txls, int col, int align);
|
||
```
|
||
设置指定列的对齐方式,方式包括有TXLS_ALIGN_LEFT,TXLS_ALIGN_RIGHT,TXLS_ALIGN_CENTER三种,其他的均为未知对齐方式。
|
||
|
||
### txls对象转储
|
||
```c
|
||
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对象加载
|
||
```c
|
||
txls_t txls_loads(const char* text);
|
||
txls_t txls_file_load(const char* filename);
|
||
```
|
||
类似转储的方法,txls对象可以从字符串文本中加载,也可以从文件中加载。加载成功则会返回一个txls对象,失败则返回NULL。
|
||
|
||
### txls加载错误
|
||
```c
|
||
int txls_error_info(int* line);
|
||
```
|
||
varch的txls解析器提供了精准的错误识别,在txls加载的失败的时候可以调用此方法去定位错误位置和错误的类型。返回值为1表示当前解析出错了,0则为未出错。line输出错误所在行,type输出错误的类型。错误类型定义如下:
|
||
```c
|
||
#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文件
|
||
```c
|
||
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**
|
||
```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 |
|
||
| email | 123321@qq.com | lisi@163.com | ww1234567890@qq.com |
|
||
|
||
例子里面使用函数很多没有对返回值进行判断,实际应用需对返回值进行判断。
|
||
|
||
### 加载txls文件
|
||
在上面生成的文件的基础上,进行加载该文件。
|
||
加载测试代码
|
||
```c
|
||
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类型声明
|
||
```c
|
||
typedef struct TXLS *txls_t;
|
||
```
|
||
使用时候,只是用`txls_t`即可。
|
||
|
||
```c
|
||
/* 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**,就是这个迭代器记录列访问时候的位置,当再一次访问检查到是同位置时候,就可以快速返回,而不用从头到尾再遍历。迭代器后续再说明。
|
||
|
||
```c
|
||
/* 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(整齐输出时候的列的宽度)。
|
||
|
||
```c
|
||
/* smallest memory cell */
|
||
typedef struct CELL
|
||
{
|
||
struct CELL *next; /* next cell */
|
||
char* address; /* address of cell content */
|
||
} CELL;
|
||
```
|
||
再看CELL结构体,包含2个成员,next(指向下一个CELL,形成单向链表),address(单元格内容,字符串)。
|
||
|
||
```c
|
||
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链表迭代为说明,说明下这个迭代器获取的链表结点的过程:
|
||
```c
|
||
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),那么就可以根据列宽预测到需要多少的空间,不整齐输出则需要在转储时候动态调整大小。
|
||
现在先来看维护这个打印空间的结构体:
|
||
```c
|
||
typedef struct
|
||
{
|
||
char* address; /* buffer base address */
|
||
int size; /* size of buffer */
|
||
int end; /* end of buffer used */
|
||
} BUFFER;
|
||
```
|
||
一共也就是3个成员,address(空间的基地址),size(空间的大小),end(已使用的结尾,索引)。
|
||
动态调整空间的过程:
|
||
```c
|
||
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;
|
||
}
|
||
```
|
||
以其中一段转储的代码为例,看看这个是怎么使用的
|
||
```c
|
||
if (!buf_append(buf, 2)) return 0; // 先扩展空间
|
||
buf_push(buf, '|'); // 调用函数把字符push到buf里面
|
||
buf_push(buf, '\n');
|
||
```
|
||
|
||
在转储cell的时候有个点需要注意的,就是cell是允许换行和分隔符'|'的,换行符用`<br>`来代替,分隔符'|'则转移`\|`来代替。
|
||
```c
|
||
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、首先创建一个`0x0`txls表格对象,然后在随着解析把解析到的表头、行、列这些逐个添加到txls对象,等到解析完就可以得到一个完整的txls对象
|
||
|
||
* 2、在解析之前的首先得将解析的行号以及错误重置,然后随着解析遇到换行符就让行号相应的递增,碰到解析出错了,就记录下错误的类型
|
||
|
||
* 3、然后就到了真正的解析过程,解析分为两步走,第一步先解析表头和表格分割行,这一步是确认这个表格一共有多少列,对齐方式,以及语法符不符合表头语法。第二步,就在确定的列的基础上,逐行的解析,解析每一行所区分的单元格并把单元格内容归类到指定行列里面。
|
||
|
||
整个解析过程如下
|
||
```c
|
||
/* 创建一个空表格 */
|
||
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增删改查的其实已经没什么特别说明的了,就是对链表数据结构的基本操作,这里不着重说明了
|
||
|