varch/doc/varch:txls解析器.md
2024-04-22 00:09:51 +08:00

19 KiB
Raw Blame History

介绍

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_LEFTTXLS_ALIGN_RIGHTTXLS_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
email 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个成员columnscolumns的链表iteratorcolumns的迭代器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单元格链表iteratorpair的迭代器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增删改查的其实已经没什么特别说明的了就是对链表数据结构的基本操作这里不着重说明了