Closed luckyscript closed 6 years ago
写在前面:已经许久不写博客,这些天觉得前一段时间过得有点浪了,是时候得把书本拿起来,把键盘敲起来了。
前段时间,写了一个带注释的JSON的词法分析。从刚开始的信誓旦旦,到后来的迷茫,再到代码的重构。整个过程像是在坐过山车一样起伏,加上自己对npm包不熟悉,也顺便发布了一个包到npm上。也算是自己的第一个包,准备会一直维护。
npm install json_with_comments_parser;
export default { parse(content) { if (typeof content != "string") content = JSON.stringify(content); if (!content) return; let result = []; let contentArray = content.split(""); if (contentArray[0] == "{") { // object like json result = objectJsonParser(content.split("")); } else if (contentArray.shift() == "[") { // array like json let arrayLike = JSON.parse(content); if (arrayLike.length == 0) return result; arrayLike.forEach((v, i) => { result.push(objectJsonParser(JSON.stringify(v))); }); } else { console.error("not a valid json"); } return result; } };
主函数如此,第一版我的操作是,把传入的json字符串 转成一个数组,然后开始遍历这个数组,通过对遍历到的值,得到一些判断。主函数这里主要判断是object类型的json还是数组类型的json。然而这里有个问题是,我没有对字符串trim,所以当json前面有空格或者tab的时候,都会认为是非法的。这点当时也是忽略了。
trim
接着,主函数中主要任务就是把内容传入objectJsonParser中。
objectJsonParser
... for (char of contentArray) { // if {, push if (char == "{" && !isStringValue) { //// console.log("caaa",char); if (counter > 0) { stack.push(char); } counter += 1; } else if (char == "}" && !isStringValue) { if (counter > 1) { stack.push(char); } counter -= 1; } ...
这里主要是 做了字符的判断,通过counter来判断处于object的层级。同时还要注意,当前是否在字符串内。所以这个判断条件也是很恶心了。
if (char == ":" && counter == 1 && !isArrayValue && !isStringValue) { //// console.log("caaa",char); object.key = stack.join("").trim(); valueEnd = false; isSemiPush = false; isPushedValue = false; stack = []; } else if ( char == "," && counter == 1 && !isArrayValue && !isStringValue ) { object.children = typeof objectJsonParser( stack .join("") .trim() .split("") ) == "string" ? [] : objectJsonParser( stack .join("") .trim() .split("") ); object.value = delEmpty(stack) .join("") .trim(); object.type = judgeType(delEmpty(stack)); valueEnd = true; // result.push({...object}); result.push(JSON.parse(JSON.stringify(object))); stack = []; isSemiPush = true; }
当然 更恶心的还在这个地方,比如':'的判断,这里我之前就没考虑到 字符串中包含冒号和数组中包含冒号的情况,导致判断条件写的越来越臃肿。随意感受一个这个代码,不但“抽象”,而且丝毫没有维护性,定位bug也是需要定位半天。所以,在此基础上,我决定重构代码。
第二版主要使用typescript来写,因为我觉得 写这些包不涉及node,不涉及dom,用ts的强类型还是一个比较好的选择,刚好自己不怎么会ts,借此机会来学习一下。
import parser from './parser' export function parse (json:string) { let jsonText = json.trim(); console.log("【jsontext】=> ", jsonText); return parser(jsonText); }
入口函数是这样的,其实就是一段废话,当然这里做了trim的操作,也是有了前车之鉴。
function parse (json:string):any { if(!check_valid(json)) throw new Error("Not valid JSON"); if(json[0] === '{') { return { value: parse_object(json).value, type: 'Object' }; } else if(json[0] == '[') { // array like json return { value: parse_value_array(json).children||parse_value_array(json).value, type: 'Array' }; } else { // if comment is before json, throw error throw new Error(`unsupport input: ${json}`) } }
主函数 parse,思路还是和之前的很像,这里也不赘述。
parse
let parse_object = (value:string):Array<Tree>|any => { let len:number = value.length; let pointer:number = 1; let tree:Tree = { key: '', value: '', comment: '', children: [], type: 'Object' } let result: Array<Tree>|any = []; let inKey:boolean = false, inValue: boolean = false, inComment:boolean = false; let nextType = 'key'; let stack:Array<string> = []; let commentFlag = false; // skip whitespace pointer = skip_whitespace(value, pointer); if(value[pointer] == '}') { result = []; return {value: result}; } for(let depth = 1;pointer < len && depth !== 0;pointer++) { // key start let char:string = value[pointer]; if(inKey) { if(char == '"') { // key end inKey = false; nextType = 'value'; tree.key = stack.join(""); stack = []; pointer = skip_whitespace(value, pointer); commentFlag = true; } else { stack.push(char); } } if(char == '"' && nextType == 'key') { // stack.push(char) inKey = true; // result.push(JSON.parse(JSON.stringify(tree)) tree = { key: '', value: '', children: [], type: 'Object', comment: '' }; } if(inValue) { let val = parse_value(value.substr(pointer)); pointer += val.len; if(val.type == 'Object') { tree.children = val.value } else { tree.value = val.value; } tree.type = val.type; result.push(JSON.parse(JSON.stringify(tree))); inValue = false; nextType = 'key'; } if(char == ':' && !inValue && !inComment) { pointer = skip_whitespace(value, pointer); inValue = true; } if(char == '/' && value[pointer - 1] == '/' && !inValue && !inKey && commentFlag) { // sigle line comment start let comment = parse_single_comment(value.substr(pointer)); pointer += comment.len; tree.comment = comment.comment; result.pop(); result.push(JSON.parse(JSON.stringify(tree))); } if(char == '*' && value[pointer - 1] == '/' && !inValue && !inKey) { // sigle line comment start let comment = parse_multi_comment(value.substr(pointer)); pointer += comment.len; tree.comment = comment.comment; result.pop(); result.push(JSON.parse(JSON.stringify(tree))); } if(char == '{') depth++; if(char == '}') depth--; } return { value: result, len: pointer + 1, type: 'Object' }; }
object类型 的parse函数,这里我将 值为object和最外层的object统一了,用递归的方法去调用。在最后用depth来标志object的层级。用pointer,来获取char的同时,也能知道当前的位置。在获取值的时候,直接调用parse_value。在匹配到注释的时候,直接调用parse_single_comment或者parse_multi_comment。唯一需要判断的就是inkey还是invalue。思路还是比较清晰。
let parse_value = function (value: string) { value = value.trim(); switch(value[0]) { // bool case 't': return parse_value_literal(value, 'true') case 'f': return parse_value_literal(value, 'false'); // null case 'n': return parse_value_literal(value, 'null'); // string case '"': return parse_value_string(value); // array case '[': return parse_value_array(value); // object case '{': return parse_object(value); default: return parse_number(value); } }
parsevalue中,主要就是一个switch case语句,通过特征来匹配到对应的值的解析中。
比如number就会进入 parse_number中。
let parse_number = (value:string) => { let p = 0; if(value[p] == '-') p++; if(value[p] == '0') p++; else { if(!ISDIGIT1TO9(value[p])) throw new Error('not a valid number'); for(p++; ISDIGIT(value[p]); p++); } if (value[p] == '.') { p++; if (!ISDIGIT(value[p])) throw new Error('not a valid number'); for (p++; ISDIGIT(value[p]); p++); } if (value[p] == 'e' || value[p] == 'E') { p++; if (value[p] == '+' || value[p] == '-') p++; if (!ISDIGIT(value[p])) throw new Error('not a valid number'); for (p++; ISDIGIT(value[p]); p++); } return { value: value.substr(0, p), type: 'Number', len: p } }
虽然输入的值 可能会多余,但是 只匹配到 对应的值,然后返回。
源码在:https://github.com/luckyscript/json_parser
前段时间,写了一个带注释的JSON的词法分析。从刚开始的信誓旦旦,到后来的迷茫,再到代码的重构。整个过程像是在坐过山车一样起伏,加上自己对npm包不熟悉,也顺便发布了一个包到npm上。也算是自己的第一个包,准备会一直维护。
第一版思路浅析:
主函数如此,第一版我的操作是,把传入的json字符串 转成一个数组,然后开始遍历这个数组,通过对遍历到的值,得到一些判断。主函数这里主要判断是object类型的json还是数组类型的json。然而这里有个问题是,我没有对字符串
trim
,所以当json前面有空格或者tab的时候,都会认为是非法的。这点当时也是忽略了。接着,主函数中主要任务就是把内容传入
objectJsonParser
中。这里主要是 做了字符的判断,通过counter来判断处于object的层级。同时还要注意,当前是否在字符串内。所以这个判断条件也是很恶心了。
当然 更恶心的还在这个地方,比如':'的判断,这里我之前就没考虑到 字符串中包含冒号和数组中包含冒号的情况,导致判断条件写的越来越臃肿。随意感受一个这个代码,不但“抽象”,而且丝毫没有维护性,定位bug也是需要定位半天。所以,在此基础上,我决定重构代码。
第二版思路浅析
第二版主要使用typescript来写,因为我觉得 写这些包不涉及node,不涉及dom,用ts的强类型还是一个比较好的选择,刚好自己不怎么会ts,借此机会来学习一下。
入口函数是这样的,其实就是一段废话,当然这里做了trim的操作,也是有了前车之鉴。
主函数
parse
,思路还是和之前的很像,这里也不赘述。object类型 的parse函数,这里我将 值为object和最外层的object统一了,用递归的方法去调用。在最后用depth来标志object的层级。用pointer,来获取char的同时,也能知道当前的位置。在获取值的时候,直接调用parse_value。在匹配到注释的时候,直接调用parse_single_comment或者parse_multi_comment。唯一需要判断的就是inkey还是invalue。思路还是比较清晰。
parsevalue中,主要就是一个switch case语句,通过特征来匹配到对应的值的解析中。
比如number就会进入 parse_number中。
虽然输入的值 可能会多余,但是 只匹配到 对应的值,然后返回。
源码在:https://github.com/luckyscript/json_parser