c++TinML转html
- 前言
- 解析
- 解释
- 转译html
- 类定义
- 开头html
- 结果
- 这是最终效果(部分): 
前言
在python.tkinter设计标记语言(转译2-html)中提到了将TinML转为静态html的python实现方法;在HtmlRender - c++实现的html生成类中又提供了我自己写的基于c++编辑html的简单方式,这篇笔记就是使用c++将tinml转为静态html。
本示例未使用任何第三方库(除了我之前提供的HtmlRender类),但是在html中代码高亮方面使用了在线的highlight.js
。
整套流程下来和基于python的TinText平台类似,都是先解析TinML标记文本段,再给每个标记赋以特定含义(解释过程),最后通过这些解释后的内容生成html结构,生成html文本。
项目示例:CppTinParser: TinML to html by c++
解析
这一部分是对TinML标记的语法解析。
class TinLexer{
public:
TinLexer(string contents){
this->content = '\n' + contents + '\n';
}
void run_test();
vector<string> run();
vector<map<string,vector<string>>> lex();
private:
string content;
};
主要解析功能就是对每一行TinML标记文本段进行基于上下文标记的正则匹配和结构解析,这部分相当于TinText项目中的TinParser
:
- 单行TinML标记,分为标记名称和标记参数组
- 多行模式TinML,转为单行TinML标记解析后格式
部分代码如下:
vector<map<string,vector<string>>> TinLexer::lex(){
// 预处理
// tackle <tinfile>: add context of the specific TinML file into this->content
// 此部分代码省略,可见程序源码
// ...
vector<map<string,vector<string>>> tinresults;//总结果
vector<string> contents;
string nowtag;//当前标签
contents = split(this->content, '\n');
bool Lines = false;//多行模式
regex pattern("^<(.*?)>(.*)");
smatch result;
vector<string> args;//参数列表
for (const auto &i : contents){
if (size(i) == 1){
// 实际上是empty,不过貌似写不对
// 既有可能是因为getline将\n转为\0,导致该字符串截止
continue;
}
if (size(i)>=2 && i.substr(0,2)=="|-"){
// cout << "Comment: " << i << endl;
continue;
}
map<string,vector<string>> lineargs;//单行参数
if (Lines){
if (i[0]=='|'){
if (size(i)>2 && i[i.length()-2]!='|'){
string content = i.substr(1,size(i)-2);
args.push_back(subreplace(content, "%VEB%", "|"));
}else if (size(i)==2){
args.push_back("");
}else{
string content = i.substr(1,size(i)-3);
args.push_back(subreplace(content, "%VEB%", "|"));
// for (auto const &j : args){
// cout << "Arg: " << j << endl;
// }
lineargs[nowtag] = args;
tinresults.push_back(lineargs);
args.clear();
Lines = false;
}
}
}else if (i[i.length()-2] == ';'){
// getline最后一个为\0,因此-2
// string本身不以\0结尾,但是getline会改成\0
Lines = true;
bool ismatch = regex_search(i, result, pattern);
if (ismatch){
// cout << "Tag: " << result[1] << endl;
nowtag = result[1];
string content = result[2];
int lastindex = content.find_last_of(';');
content = content.substr(0, lastindex);
args.push_back(subreplace(content, "%VEB%", "|"));
}else{
cout << "\033[33;1m不可被解析,当作<p>处理:" << i << "\033[0m" << endl;
args.push_back(subreplace(i, "%VEB%", "|"));
lineargs["p"] = args;
tinresults.push_back(lineargs);
args.clear();
}
}else{
bool ismatch = regex_search(i, result, pattern);
if (ismatch){
nowtag = result[1];
string content = result[2];
auto oargs = split(content, '|');
for (auto const &j : oargs)
args.push_back(subreplace(j, "%VEB%", "|"));
lineargs[nowtag] = args;
tinresults.push_back(lineargs);
args.clear();
}else{
//无法匹配当作p处理
cout << "\033[33;1m不可被解析,当作<p>处理:" << i << "\033[0m" << endl;
args.push_back(subreplace(i, "%VEB%", "|"));
lineargs["p"] = args;
tinresults.push_back(lineargs);
args.clear();
}
}
}
return tinresults;
}
这里还遇到一个坑,
string.getline
会将分割字符转为\0
,这样导致string类型变量所包含的文字比表达的文字多一个“字”。string的设计是不包含的\0
,所以要注意从string类型中取字符的下标。
解析过程是最基础的,也几乎不会再维护,毕竟TinML的语法就这样了。
解释
这和TinText软件中转译html的过程一样,只不过TinEngine在渲染之前就生成了解释内容并传递给了转译器,而CppTinParser需要单独进行参数解释。
首先,生成每个标记对应的参数含义:
static map<string, vector<string>> tinkws;
void loadkws()
{
// 标记对应的参数键
tinkws["ac"] = {"name"};
tinkws["anchor"] = {"name"};
tinkws["code"] = {"type", "codes"};
tinkws["fl"] = {};
tinkws["follow"] = {};
tinkws["html"] = {"htmls"};
tinkws["img"] = {"name", "url", "size"};
tinkws["image"] = {"name", "url", "size"};
tinkws["lnk"] = {"text", "url", "description"};
tinkws["link"] = {"text", "url", "description"};
tinkws["a"] = {"text", "url", "description"};
tinkws["ls"] = {"lists"};
tinkws["list"] = {"lists"};
tinkws["n"] = {"notes"};
tinkws["note"] = {"notes"};
tinkws["nl"] = {"lists"};
tinkws["numlist"] = {"lists"};
tinkws["p"] = {"texts"};
tinkws["pages"] = {"name"};
tinkws["/page"] = {""};
tinkws["/pages"] = {""};
// 展示到这里位置,当然,这不是完整的
//...
}
由于之前已经生成了类似如下的数据结构:
(
("<title>",("title","1"),),
("<p>",("paragraph",)),
...
)
现在,就需要把这些已经分割好的标记文本转为有意义的内容:
(
("<title>",("title": "title", "level": "1"),),
("<p>",("paragraph": ("paragraph",))),
...
)
这里采用的逻辑是对一个标签所拥有的所有参数一一对应,多出来的统一给最后一个参数。
这部分逻辑实现如下:
map<string, string> tokeywords(string tag, vector<string> contents)
{
// 将列表顺序参数转为键、值类参数
map<string, string> keywords;
cout << "Tag: " << tag << endl;
keywords["tag"] = tag;
int args_num = contents.size();
int index = 0;
for (auto kws : tinkws[tag])
{
if (index >= args_num)
{
// 剩下键全为空
keywords[kws] = "";
continue;
}
keywords[kws] = contents[index];
// cout << kws << " = " << contents[index] << endl;
index++;
}
if (index < args_num)
{
// 将剩下的参数全部放入最后一个键中
for (int i = index; i < args_num; i++)
{
keywords[tinkws[tag].back()] += "\n" + contents[i];
}
}
for (auto kw : keywords)
{
if (kw.first == "tag")
{ // 跳过tag键
continue;
}
cout << kw.first << " = " << kw.second << endl;
}
return keywords;
}
class TinParser
{
public:
TinParser(vector<map<string, vector<string>>> contents)
{
this->contents = contents;
}
void parse();
void render(string file);
private:
vector<map<string, vector<string>>> contents;
vector<map<string, string>> result;
};
void TinParser::parse()
{
// 判断tag是否存在
contents = this->contents;
for (auto content : contents)
{
string tag = content.begin()->first;
if (tinkws.find(tag) == tinkws.end())
{
cerr << "\033[33;1mtag not found: " << tag << "\033[0m" << endl;
// //string转char
// char c_tag;
// strcpy(&c_tag, tag.c_str());
// throw NoTagName(c_tag);
continue;
}
map<string, string> results = tokeywords(tag, content.begin()->second);
result.push_back(results);
}
}
转译html
经过前面的准备,这里已经准备好了将TinML转译为html的所有信息。
由于在之前的文章中已经给出了HtmlRender
的实现和使用示例,这里便不再赘叙。
这部分的代码和TinText的tin2html.py
实现在逻辑上完全一致,甚至可以看作是python到c++的翻译。
类定义
class TinRender {
//tin->html类
public:
TinRender(const string &file) : file_(file){}
HtmlRender* render(vector<map<string,string>> content, bool _style = true, bool _code_style = true);//转译为html
void output(vector<map<string,string>> content);//输出
private:
string file_;//html文件路径
ofstream fout_;//html文件输出流
};
开头html
就是html头中包含的样式、额外引入js、标题这些。
HtmlRender* TinRender::render(vector<map<string, string>> content, bool _style, bool _code_style){
//转译为HtmlRender。可以直接使用output方法转译+输出保存
//注意,CPPTinParser只是用c++实现的tinml转译器
//不作为TinML规范的检查器
string tag;
HtmlRender* html = new HtmlRender("html", "", {});
HtmlRender* head = new HtmlRender("head", "", {});
HtmlRender* title = new HtmlRender("title", "TinML", {});
head->add(title);
HtmlRender* meta = new HtmlRender("meta", "", {{"charset","UTF-8"}}, true);
head->add(meta);
//读取css文件
if (_style){
string filename = "blubook.css";
ifstream infile;
infile.open(filename.data()); //将文件流对象与文件连接起来
assert(infile.is_open()); //若失败,则输出错误消息,并终止程序运行
string s, strAllLine;
while (getline(infile, s)){
strAllLine += s + "\n";
}
infile.close(); //关闭文件输入流
HtmlRender* style = new HtmlRender ("style", strAllLine, {}, false, true);
head->add(style);
}
//<code>样式
if (_code_style){
HtmlRender* codestyle = new HtmlRender("link", "", {{"rel","stylesheet"}, {"type", "text/css"}, {"href","https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs.min.css"}});
head->add(codestyle);
HtmlRender* codescript = new HtmlRender("script", "", {{"src","https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"}});
head->add(codescript);
HtmlRender* codestartscript = new HtmlRender("script", "hljs.highlightAll();", {});
head->add(codestartscript);
}
HtmlRender* _body = new HtmlRender("body", "", {});
HtmlRender* body = new HtmlRender("div", "", {{"id","content"}});
_body->add(body);
html->add(head);
html->add(_body);
// 提前定义类型变量
HtmlRender* table;
HtmlRender* tbody;
bool tablehead = false;
HtmlRender* tabsview;
bool pagestag = false;
vector<string> pagesnames;
vector<map<string, string>> pagescontent;
int pagescount = 0;
// ...
}
然后就是对每一个标记的单独转译,这些都在上面的那个方法里,这里给几个例子:
// ...
//常规转译TinML解析结果
if (tag == "ac" || tag == "anchor"){
string name = item["name"];
if (name[0]=='#'){
//锚点链接
HtmlRender* anchor = new HtmlRender("a", "🔗", {{"href", name}});
HtmlRender* lastitem = body->children().back();
lastitem->add(anchor);
}else{
//锚点定义
HtmlRender* anchor = new HtmlRender("a", "", {{"id", name}});
body->add(anchor);
}
}else if (tag == "code"){
//代码块
//使用highlight.js,不支持tinml代码块
string type = item["type"];
string codes = item["codes"];
if (type == "tin"){
type = "nohighlight";
}else{
type = "language-" + type;
}
HtmlRender* pre = new HtmlRender("pre", "", {});
HtmlRender* code = new HtmlRender("code", codes, {{"class",type}});
pre->add(code);
body->add(pre);
}else if (tag == "html"){
//html文本
string htmltext = item["htmls"];
HtmlRender* htmlcont = new HtmlRender("", htmltext, {}, false, true);
body->add(htmlcont);
}else if (tag == "img" || tag == "image"){
//图片
string name = item["name"];
string url = item["url"];
if (url.empty()){
//因为CppTinParser只是用c++实现的tinml转译器
//不作为TinML规范的检查器,也不作为tin文件渲染环境
//所以不会有额外的资源文件储存位置
//因此,不支持从本地文件中导入图片
continue;
}
vector<string> size = render_split(item["size"], 'x');
string width = size[0];
string height = size[1];
HtmlRender* img = new HtmlRender("img", "", {{"alt", ""},{"src", url}, {"width", width}, {"height", height}});
body->add(img);
}// ...
// ...
CppTinParser甚至给对<p>
的解析也单独移植了TinText中的逻辑:
void load_para(HtmlRender* p, vector<string> lines){
//添加<p>内容
vector<char> tags = {'*','-','_','/','=','!','^','&','#'};
regex reg(".*?!\\[(.*)\\]\\((.*)\\)");
smatch result;
int count = 0;
if (lines.size()==0){
//空行
HtmlRender* br = new HtmlRender("br", "", {}, true);
p->add(br);
return;
}
for (auto &line : lines) {
HtmlRender* last = p;//p最后一个元素
if (line[0] == ' '){
//空格
string text = line.substr(1);
HtmlRender* p_item = new HtmlRender("", text, {});
p->add(p_item);
continue;
}
string head;
if (line.size() <=9){
head = line;
}else{
head = line.substr(0, 9);
}
//<p>开头标记需要连续
count = 0;
for (auto &tag_char : head){
if (find(tags.begin(), tags.end(), tag_char) != tags.end()){
count++;
}else{
break;
}
}
head = head.substr(0, count);
if (count == 0){
HtmlRender* p_item = new HtmlRender("", line, {});
p->add(p_item);
continue;
}
if (head.find('*')!=string::npos){
HtmlRender* nowlast = new HtmlRender("b", "", {});
last->add(nowlast);
last = nowlast;
}
if (head.find('/')!=string::npos){
HtmlRender* nowlast = new HtmlRender("i", "", {});
last->add(nowlast);
last = nowlast;
}
if (head.find('_')!=string::npos){
HtmlRender* nowlast = new HtmlRender("u", "", {});
last->add(nowlast);
last = nowlast;
}
if (head.find('-')!=string::npos){
HtmlRender* nowlast = new HtmlRender("s", "", {});
last->add(nowlast);
last = nowlast;
}
if (head.find('=')!=string::npos){
// = 和 # 只能存在一个
HtmlRender* nowlast = new HtmlRender("mark", "", {});
last->add(nowlast);
last = nowlast;
}else if (head.find('#')!=string::npos){
HtmlRender* nowlast = new HtmlRender("code", "", {});
last->add(nowlast);
last = nowlast;
}
if (head.find('^')!=string::npos){
// ^ 和 & 只能存在一个
HtmlRender* nowlast = new HtmlRender("sup", "", {});
last->add(nowlast);
last = nowlast;
}else if (head.find('&')!=string::npos){
HtmlRender* nowlast = new HtmlRender("sub", "", {});
last->add(nowlast);
last = nowlast;
}
if (head.find('!')!=string::npos){
bool ismatch = regex_match(line, result, reg);
if (ismatch) {
string text = result[1];
string url = result[2];
if (text == ""){
text = url;
}
HtmlRender* p_item = new HtmlRender("a", text, {{"href",url}});
last->add(p_item);
}else{
string text = line.substr(count);
HtmlRender* p_item = new HtmlRender("", text, {});
last->add(p_item);
}
}else{
last->configcnt(line.substr(count));
}
}
}
结果
这里用的是TinText项目中的test.tin
。效果上来看,除了<code>
中对TinML的缺失以及对在线图片、tinfile不支持外,其他几乎一样,而且对于大文件的转译绝对比python实现要快。
这是源文件(部分):