## 介绍 txls是varch的文本表格,语法参考markdown表格的语法,为了更好的文本直观性,txls语法规则比markdown表格语法稍微严格一点。通过txls,可以很方便的访问一个txls文件的行列内容,以及生成一个整齐规范可读性高的文本形式的表格文件。 简单介绍下txls规范,以及对比下markdown表格的语法。 **表头** ``` 表头和markdown表格表头很类似,只不过稍微严格点 markdown表格不要求当前行两端一定需要`|`分隔符,但是txls为了更整齐的格式,要求行的两端必须包含`|`分割符。后面的每一行内容也有如此要求。 表头后面跟随分割行,分割行的列数需与表头列数一致 分割行的内容必须包含连续的`-` ``` 例子: ```md | col 1 | col 2 | col 3 | col 4 | col 5 | |-------|-------|--------------------|-------|-------| ``` **行** ``` 在一行中,以`|`分隔符作为区分列 在`|`分隔符之间,包含的即为单元格内容,单元格内容不包含两端的空格部分 单元格内容内容可以包换转义字符 "\|" 表示 '|',"
" 表示 '\n' ``` 例子: ``` | col 1 | Zhang san | col 3 | col 4 | col 5 | |-------|----------:|----------------|-------|-------| | 11 | 21 | 31 | 41 | 51 | | 12 | 22 | 1234\|
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 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是允许换行和分隔符'|'的,换行符用`
`来代替,分隔符'|'则转移`\|`来代替。 ```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增删改查的其实已经没什么特别说明的了,就是对链表数据结构的基本操作,这里不着重说明了