web-utility
Version:
Web front-end toolkit based on TypeScript
179 lines (145 loc) • 4.69 kB
text/typescript
import { isUnsafeNumeric, objectFrom } from './data';
export function parseJSON(raw: string) {
function parseItem(value: any) {
if (typeof value === 'string' && /^\d+(-\d{1,2}){1,2}/.test(value)) {
const date = new Date(value);
if (!Number.isNaN(+date)) return date;
}
return value;
}
const value = parseItem(raw);
if (typeof value !== 'string' || isUnsafeNumeric(value)) return value;
try {
return JSON.parse(raw, (key, value) => parseItem(value));
} catch {
return raw;
}
}
export function toJSValue(raw: string) {
const parsed = parseJSON(raw);
if (typeof parsed !== 'string') return parsed;
const number = +parsed;
return Number.isNaN(number) || number + '' !== parsed ? parsed : number;
}
export const stringifyTextTable = (data: object[], separator = ',') =>
[
Object.keys(data[0]).join(separator),
...data.map(item =>
Object.values(item)
.map(value => JSON.stringify(value))
.join(separator)
)
].join('\n');
function readQuoteValue(raw: string) {
const quote = raw[0];
const index = raw.indexOf(quote, 1);
if (index < 0) throw SyntaxError(`A ${quote} is missing`);
return raw.slice(1, index);
}
function parseRow(row: string, separator = ',') {
const list = [];
do {
let value: string;
if (row[0] === '"' || row[0] === "'") {
value = readQuoteValue(row);
row = row.slice(value.length + 3);
} else {
const index = row.indexOf(separator);
if (index > -1) {
value = row.slice(0, index);
row = row.slice(index + 1);
} else {
value = row;
row = '';
}
}
list.push(toJSValue(value.trim()));
} while (row);
return list;
}
/**
* @deprecated Since 4.6.0, please use {@link parseTextTableAsync} or {@link readTextTable}
* for better performance with large tables to avoid high memory usage
*/
export function parseTextTable<T extends Record<string, any> = {}>(
raw: string,
header?: boolean,
separator = ','
) {
const data = raw
.trim()
.split(/[\r\n]+/)
.map(row => parseRow(row, separator));
return !header
? data
: data.slice(1).map(row => objectFrom(row, data[0]) as T);
}
export const parseTextTableAsync = async <T extends Record<string, any> = {}>(
raw: string
) => Array.fromAsync(readTextTable<T>(raw[Symbol.iterator]()));
async function* characterStream(
chunks: Iterable<string> | AsyncIterable<string>
) {
for await (const chunk of chunks) yield* chunk;
}
async function* parseCharacterStream(
chars: AsyncGenerator<string>,
separator = ','
) {
let inQuote = false;
let quoteChar = '';
let prevChar = '';
let cellBuffer = '';
let currentRow: any[] = [];
const completeCell = () => {
currentRow.push(toJSValue(cellBuffer.trim()));
cellBuffer = '';
};
for await (const char of chars) {
if (char === '\n' || char === '\r') {
if (char === '\n' && prevChar === '\r') {
prevChar = char;
continue;
}
completeCell();
if (currentRow.length > 1 || currentRow[0]) yield currentRow;
currentRow = [];
} else if (
(char === '"' || char === "'") &&
!inQuote &&
cellBuffer.trim() === ''
) {
inQuote = true;
quoteChar = char;
} else if (char === quoteChar && inQuote) {
inQuote = false;
quoteChar = '';
} else if (inQuote) {
cellBuffer += char;
} else if (char === separator) {
completeCell();
} else {
cellBuffer += char;
}
prevChar = char;
}
if (cellBuffer || currentRow.length > 0) {
completeCell();
if (currentRow.length > 1 || currentRow[0]) yield currentRow;
}
}
export async function* readTextTable<T extends Record<string, any> = {}>(
chunks: Iterable<string> | AsyncIterable<string>,
header?: boolean,
separator = ','
) {
let headerRow: string[] | undefined;
let isFirstRow = true;
const chars = characterStream(chunks);
for await (const row of parseCharacterStream(chars, separator))
if (header && isFirstRow) {
headerRow = row;
isFirstRow = false;
} else
yield header && headerRow ? (objectFrom(row, headerRow) as T) : row;
}