alm
Version:
The best IDE for TypeScript
439 lines (381 loc) • 12.6 kB
text/typescript
import * as types from './types';
import * as ts from 'typescript';
export interface ParsedData<T> {
/** only if valid */
data?: T;
/** only if error */
error?: ParseError;
}
/**
* Remove the comments from a json like text.
* Comments can be single line comments (starting with # or //) or multiline comments using / * * /
*
* This method replace comment content by whitespace rather than completely remove them to keep positions in json parsing error reporting accurate.
*/
function removeComments(jsonText: string): string {
let output = "";
const scanner = ts.createScanner(ts.ScriptTarget.ES5, /* skipTrivia */ false, ts.LanguageVariant.Standard, jsonText);
let token: ts.SyntaxKind;
while ((token = scanner.scan()) !== ts.SyntaxKind.EndOfFileToken) {
switch (token) {
case ts.SyntaxKind.SingleLineCommentTrivia:
case ts.SyntaxKind.MultiLineCommentTrivia:
// replace comments with whitespace to preserve original character positions
output += scanner.getTokenText().replace(/\S/g, " ");
break;
default:
output += scanner.getTokenText();
break;
}
}
return output;
}
/**
* Over JSON.parse:
* * allows BOM
* * allows // /* # comments
* * provides a typed error detail on parse error
*/
export function parse<T>(str: string): ParsedData<T> {
const content = removeComments(stripBOM(str));
try {
return { data: json_parse(content) };
}
catch (e) {
let error: { message: string; at: number } = e;
const indexToPosition = (index: number): { line: number, ch: number } => {
let beforeLines = splitlines(content.substr(0, index));
return {
line: Math.max(beforeLines.length - 1, 0),
ch: Math.max(beforeLines[beforeLines.length - 1].length - 1, 0)
};
}
let fromIndex = Math.max(error.at - 1, 0);
let toIndex = Math.min(error.at + 1, content.length);
return {
error: {
message: e.message,
from: indexToPosition(fromIndex),
to: indexToPosition(toIndex),
preview: content.substring(fromIndex, toIndex - 1)
}
}
}
}
export function stringify(object: Object, eol: string = '\n'): string {
var cache = [];
var value = JSON.stringify(object,
// fixup circular reference
function(key, value) {
if (typeof value === 'object' && value !== null) {
if (cache.indexOf(value) !== -1) {
// Circular reference found, discard key
return;
}
// Store value in our collection
cache.push(value);
}
return value;
},
// indent 2 spaces
2);
value = value.split('\n').join(eol) + eol;
cache = null;
return value;
}
export function parseErrorToCodeError(filePath: string, error: ParseError, source: types.CodeErrorSource): types.CodeError {
return {
source,
filePath,
from: error.from,
to: error.to,
message: error.message,
preview: error.preview,
level: 'error'
}
}
function stripBOM(str: string) {
// Catches EFBBBF (UTF-8 BOM) because the buffer-to-string
// conversion translates it to FEFF (UTF-16 BOM)
if (typeof str === 'string' && str.charCodeAt(0) === 0xFEFF) {
return str.slice(1);
}
return str;
}
function splitlines(string: string) { return string.split(/\r\n?|\n/); };
export interface ParseError {
message: string;
preview: string;
from: {
/** zero based */
line: number;
/** zero based */
ch: number
};
to: {
/** zero based */
line: number;
/** zero based */
ch: number
};
}
/**
* https://github.com/douglascrockford/JSON-js/blob/master/json_parse.js
* AS IT IS. ONLY MODIFIED WITH TYPE ASSERTSIONS / ANNOTATIONS
*/
var json_parse: any = (function() {
"use strict";
// This is a function that can parse a JSON text, producing a JavaScript
// data structure. It is a simple, recursive descent parser. It does not use
// eval or regular expressions, so it can be used as a model for implementing
// a JSON parser in other languages.
// We are defining the function inside of another function to avoid creating
// global variables.
var at, // The index of the current character
ch, // The current character
escapee = {
'"': '"',
'\\': '\\',
'/': '/',
b: '\b',
f: '\f',
n: '\n',
r: '\r',
t: '\t'
},
text,
error = function(m) {
// Call error when something is wrong.
throw {
name: 'SyntaxError',
message: m,
at: at,
text: text
};
},
next = function(c?) {
// If a c parameter is provided, verify that it matches the current character.
if (c && c !== ch) {
error("Expected '" + c + "' instead of '" + ch + "'");
}
// Get the next character. When there are no more characters,
// return the empty string.
ch = text.charAt(at);
at += 1;
return ch;
},
number = function() {
// Parse a number value.
var number,
string = '';
if (ch === '-') {
string = '-';
next('-');
}
while (ch >= '0' && ch <= '9') {
string += ch;
next();
}
if (ch === '.') {
string += '.';
while (next() && ch >= '0' && ch <= '9') {
string += ch;
}
}
if (ch === 'e' || ch === 'E') {
string += ch;
next();
if (ch === '-' || ch === '+') {
string += ch;
next();
}
while (ch >= '0' && ch <= '9') {
string += ch;
next();
}
}
number = +string;
if (!isFinite(number)) {
error("Bad number");
} else {
return number;
}
},
string = function() {
// Parse a string value.
var hex,
i,
string = '',
uffff;
// When parsing for string values, we must look for " and \ characters.
if (ch === '"') {
while (next()) {
if (ch === '"') {
next();
return string;
}
if (ch === '\\') {
next();
if (ch === 'u') {
uffff = 0;
for (i = 0; i < 4; i += 1) {
hex = parseInt(next(), 16);
if (!isFinite(hex)) {
break;
}
uffff = uffff * 16 + hex;
}
string += String.fromCharCode(uffff);
} else if (typeof escapee[ch] === 'string') {
string += escapee[ch];
} else {
break;
}
} else {
string += ch;
}
}
}
error("Bad string");
},
white = function() {
// Skip whitespace.
while (ch && ch <= ' ') {
next();
}
},
word = function() {
// true, false, or null.
switch (ch) {
case 't':
next('t');
next('r');
next('u');
next('e');
return true;
case 'f':
next('f');
next('a');
next('l');
next('s');
next('e');
return false;
case 'n':
next('n');
next('u');
next('l');
next('l');
return null;
}
error("Unexpected '" + ch + "'");
},
value, // Place holder for the value function.
array = function() {
// Parse an array value.
var array = [];
if (ch === '[') {
next('[');
white();
if (ch === ']') {
next(']');
return array; // empty array
}
while (ch) {
array.push(value());
white();
if (ch === ']') {
next(']');
return array;
}
next(',');
white();
}
}
error("Bad array");
},
object = function() {
// Parse an object value.
var key,
object = {};
if (ch === '{') {
next('{');
white();
if (ch === '}') {
next('}');
return object; // empty object
}
while (ch) {
key = string();
white();
next(':');
if (Object.hasOwnProperty.call(object, key)) {
error('Duplicate key "' + key + '"');
}
object[key] = value();
white();
if (ch === '}') {
next('}');
return object;
}
next(',');
white();
}
}
error("Bad object");
};
value = function() {
// Parse a JSON value. It could be an object, an array, a string, a number,
// or a word.
white();
switch (ch) {
case '{':
return object();
case '[':
return array();
case '"':
return string();
case '-':
return number();
default:
return ch >= '0' && ch <= '9'
? number()
: word();
}
};
// Return the json_parse function. It will have access to all of the above
// functions and variables.
return function(source, reviver?) {
var result;
text = source;
at = 0;
ch = ' ';
result = value();
white();
if (ch) {
error("Syntax error");
}
// If there is a reviver function, we recursively walk the new structure,
// passing each name/value pair to the reviver function for possible
// transformation, starting with a temporary root object that holds the result
// in an empty key. If there is not a reviver function, we simply return the
// result.
return typeof reviver === 'function'
? (function walk(holder, key) {
var k, v, value = holder[key];
if (value && typeof value === 'object') {
for (k in value) {
if (Object.prototype.hasOwnProperty.call(value, k)) {
v = walk(value, k);
if (v !== undefined) {
value[k] = v;
} else {
delete value[k];
}
}
}
}
return reviver.call(holder, key, value);
}({ '': result }, ''))
: result;
};
}());