quick-erd
Version:
quick and easy text-based ERD + code generator for migration, query, typescript types and orm entity
300 lines (299 loc) • 9.13 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.Parser = void 0;
exports.parse = parse;
const enum_1 = require("./enum");
const meta_1 = require("./meta");
function parse(input) {
const parser = new Parser();
parser.parse(input);
return parser;
}
class Parser {
table_list = [];
line_list = [];
zoom;
view;
textBgColor;
diagramTextColor;
textColor;
diagramBgColor;
tableBgColor;
tableTextColor;
parse(input) {
input.split('\n').forEach(line => {
line = line
.trim()
.replace(/#.*/, '')
.replace(/\/\/.*/, '')
.trim();
if (!line)
return;
this.line_list.push(line);
});
this.table_list = [];
while (this.hasTable()) {
this.table_list.push(this.parseTable());
}
this.parseMeta(input);
}
parseMeta(input) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-non-null-asserted-optional-chain
const zoom = +input.match(meta_1.zoomValueRegex)?.[1];
if (zoom)
this.zoom = zoom;
const view = input.match(meta_1.viewPositionRegex);
if (view)
this.view = { x: +view[1], y: +view[2] };
const textBgColor = input.match(meta_1.textBgColorRegex);
if (textBgColor)
this.textBgColor = textBgColor[1];
const textColor = input.match(meta_1.textColorRegex);
if (textColor)
this.textColor = textColor[1];
const diagramBgColor = input.match(meta_1.diagramBgColorRegex);
if (diagramBgColor)
this.diagramBgColor = diagramBgColor[1];
const diagramTextColor = input.match(meta_1.diagramTextColorRegex);
if (diagramTextColor)
this.diagramTextColor = diagramTextColor[1];
const tableBgColor = input.match(meta_1.tableBgColorRegex);
if (tableBgColor)
this.tableBgColor = tableBgColor[1];
const tableTextColor = input.match(meta_1.tableTextColorRegex);
if (tableTextColor)
this.tableTextColor = tableTextColor[1];
input.match(meta_1.tableNameRegex_g)?.forEach(line => {
const match = line.match(meta_1.tableNameRegex) || [];
const name = match[1];
const x = +match[2];
const y = +match[3];
const color = match[4];
const table = this.table_list.find(table => table.name == name);
if (table)
table.position = { x, y, color };
});
}
peekLine() {
if (this.line_list.length === 0) {
throw new Error('no reminding line');
}
return this.line_list[0];
}
hasTable() {
while (this.line_list[0] === '')
this.line_list.shift();
return this.line_list[0] && this.line_list[1]?.startsWith('-');
}
parseTable() {
const name = this.parseName();
this.parseEmptyLine();
this.skipLine('-');
const field_list = parseAll(() => {
// skip empty lines
if (this.hasTable()) {
throw new Error('end of table');
}
return this.parseField();
});
const has_primary_key = field_list.some(field => field.is_primary_key);
if (!has_primary_key) {
const field = field_list.find(field => field.name === 'id');
if (field) {
field.is_primary_key = true;
}
}
return { name, field_list };
}
parseField() {
const field_name = this.parseName();
let type = '';
let is_null = false;
let is_unique = false;
let is_primary_key = false;
let is_unsigned = false;
let default_value;
let references;
for (;;) {
const name = this.parseType();
if (!name)
break;
switch (name.toUpperCase()) {
case 'NULL':
is_null = true;
continue;
case 'UNIQUE':
is_unique = true;
continue;
case 'UNSIGNED':
is_unsigned = true;
continue;
case 'DEFAULT':
// TODO parse default value
default_value = this.parseDefaultValue();
continue;
case 'PK':
is_primary_key = true;
continue;
case 'FK':
references = this.parseForeignKeyReference(field_name);
continue;
default:
if (type) {
console.debug('unexpected token:', {
field_name,
type,
token: name,
});
continue;
}
type = name;
}
}
type ||= defaultFieldType;
this.skipLine();
return {
name: field_name,
type,
is_null,
is_unique,
is_primary_key,
is_unsigned,
default_value,
references,
};
}
skipLine(line = '') {
if (this.line_list[0]?.startsWith(line)) {
this.line_list.shift();
}
}
parseEmptyLine() {
const line = this.line_list[0]?.trim();
if (line !== '') {
throw new NonEmptyLineError(line);
}
this.line_list.shift();
}
parseName() {
let line = this.peekLine();
const match = line.match(/[a-zA-Z0-9_]+/);
if (!match) {
throw new ParseNameError(line);
}
const name = match[0];
line = line.replace(name, '').trim();
this.line_list[0] = line;
return name;
}
parseType() {
let line = this.peekLine();
let match = line.match(/^not null\s*/i);
if (match) {
line = line.slice(match[0].length);
}
match = line.match(/^\w+\(.*?\)/) || line.match(/^[a-zA-Z0-9_(),"']+/);
if (!match) {
return;
}
const name = match[0];
line = line.replace(name, '').trim();
this.line_list[0] = line;
if (name.match(/^enum/i)) {
return (0, enum_1.formatEnum)(name);
}
return name;
}
parseDefaultValue() {
let line = this.peekLine();
let end;
if (line[0] === '"') {
end = line.indexOf('"', 1) + 1;
}
else if (line[0] === "'") {
end = line.indexOf("'", 1) + 1;
}
else if (line[0] === '`') {
end = line.indexOf('`', 1) + 1;
}
else if (line.includes(' ')) {
end = line.indexOf(' ');
}
else {
end = line.length - 1;
}
const value = line.slice(0, end + 1);
line = line.replace(value, '').trim();
this.line_list[0] = line;
return value;
}
parseRelationType() {
let line = this.peekLine();
const match = line.match(/.* /);
if (!match) {
throw new ParseRelationTypeError(line);
}
const type = match[0].trim();
line = line.replace(match[0], '').trim();
this.line_list[0] = line;
return type;
}
parseForeignKeyReference(ref_field_name) {
if (ref_field_name.endsWith('_id') && this.peekLine() === '') {
return {
table: ref_field_name.replace(/_id$/, ''),
field: 'id',
type: defaultRelationType,
};
}
const type = this.parseRelationType();
const table = this.parseName();
const line = this.peekLine();
let field;
if (line == '') {
field = 'id';
}
else if (line.startsWith('.')) {
this.line_list[0] = line.slice(1);
field = this.parseName();
}
else {
throw new ParseForeignKeyReferenceError(line);
}
return { type, table, field };
}
}
exports.Parser = Parser;
class NonEmptyLineError extends Error {
}
class LineError extends Error {
line;
constructor(line, message) {
super(message);
this.line = line;
}
}
class ParseNameError extends LineError {
}
class ParseRelationTypeError extends LineError {
}
class ParseForeignKeyReferenceError extends LineError {
line;
constructor(line) {
super(line, `expect '.', got '${line[0]}'`);
this.line = line;
}
}
function parseAll(fn) {
const result_list = [];
for (;;) {
try {
result_list.push(fn());
}
catch (error) {
return result_list;
}
}
}
const defaultFieldType = 'integer';
const defaultRelationType = '>0-';
;