SequenceDiagram/scripts/sequence/Parser.js

349 lines
7.1 KiB
JavaScript

define(() => {
'use strict';
function execAt(str, reg, i) {
reg.lastIndex = i;
return reg.exec(str);
}
function unescape(match) {
const c = match[1];
if(c === 'n') {
return '\n';
}
return match[1];
}
const TOKENS = [
{start: /#/y, end: /(?=\n)|$/y, omit: true},
{start: /"/y, end: /"/y, escape: /\\(.)/y, escapeWith: unescape},
{start: /'/y, end: /'/y, escape: /\\(.)/y, escapeWith: unescape},
{start: /(?=[^ \t\r\n:+\-<>,])/y, end: /(?=[ \t\r\n:+\-<>,])|$/y},
{start: /(?=[+\-<>])/y, end: /(?=[^+\-<>])|$/y},
{start: /,/y, prefix: ','},
{start: /:/y, prefix: ':'},
{start: /\n/y, prefix: '\n'},
];
const BLOCK_TYPES = {
'if': {type: 'block begin', mode: 'if', skip: []},
'else': {type: 'block split', mode: 'else', skip: ['if']},
'elif': {type: 'block split', mode: 'else', skip: []},
'repeat': {type: 'block begin', mode: 'repeat', skip: []},
};
const CONNECTION_TYPES = {
'->': {line: 'solid', left: false, right: true},
'<-': {line: 'solid', left: true, right: false},
'<->': {line: 'solid', left: true, right: true},
'-->': {line: 'dash', left: false, right: true},
'<--': {line: 'dash', left: true, right: false},
'<-->': {line: 'dash', left: true, right: true},
};
const TERMINATOR_TYPES = [
'none',
'box',
'cross',
'bar',
];
const NOTE_TYPES = {
'note': {
mode: 'note',
multiAgent: true,
types: {
'over': {type: 'note over', skip: []},
'left': {type: 'note left', skip: ['of']},
'right': {type: 'note right', skip: ['of']},
'between': {type: 'note between', skip: []},
},
},
'state': {
mode: 'state',
multiAgent: false,
types: {
'over': {type: 'note over', skip: []},
},
},
};
const AGENT_MANIPULATION_TYPES = {
'define': {type: 'agent define'},
'begin': {type: 'agent begin', mode: 'box'},
'end': {type: 'agent end', mode: 'cross'},
};
function tokFindBegin(src, i) {
for(let j = 0; j < TOKENS.length; ++ j) {
const block = TOKENS[j];
const match = execAt(src, block.start, i);
if(match) {
return {
newBlock: block,
end: !block.end,
append: (block.prefix || ''),
skip: match[0].length,
};
}
}
return {
newBlock: null,
end: false,
append: '',
skip: 1,
};
}
function tokContinuePart(src, i, block) {
if(block.escape) {
const match = execAt(src, block.escape, i);
if(match) {
return {
newBlock: null,
end: false,
append: block.escapeWith(match),
skip: match[0].length,
};
}
}
const match = execAt(src, block.end, i);
if(match) {
return {
newBlock: null,
end: true,
append: '',
skip: match[0].length,
};
}
return {
newBlock: null,
end: false,
append: src[i],
skip: 1,
};
}
function tokAdvance(src, i, block) {
if(block) {
return tokContinuePart(src, i, block);
} else {
return tokFindBegin(src, i);
}
}
function skipOver(line, start, skip, error = null) {
if(skip.some((token, i) => (line[start + i] !== token))) {
if(error) {
throw new Error(error + ': ' + line.join(' '));
} else {
return start;
}
}
return start + skip.length;
}
function parseCommaList(tokens) {
const list = [];
let current = '';
tokens.forEach((token) => {
if(token === ',') {
if(current) {
list.push(current);
current = '';
}
} else {
current += (current ? ' ' : '') + token;
}
});
if(current) {
list.push(current);
}
return list;
}
function parseBlockCommand(line) {
if(line[0] === 'end' && line.length === 1) {
return {type: 'block end'};
}
const type = BLOCK_TYPES[line[0]];
if(!type) {
return null;
}
let skip = 1;
if(line.length > skip) {
skip = skipOver(line, skip, type.skip, 'Invalid block command');
}
skip = skipOver(line, skip, [':']);
return {
type: type.type,
mode: type.mode,
label: line.slice(skip).join(' '),
};
}
function parseAgentCommand(line) {
const type = AGENT_MANIPULATION_TYPES[line[0]];
if(!type) {
return null;
}
if(line.length <= 1) {
return null;
}
return Object.assign({
agents: parseCommaList(line.slice(1)),
}, type);
}
function parseNote(line) {
const mode = NOTE_TYPES[line[0]];
const labelSplit = line.indexOf(':');
if(!mode || labelSplit === -1) {
return null;
}
const type = mode.types[line[1]];
if(!type) {
return null;
}
let skip = 2;
skip = skipOver(line, skip, type.skip);
const agents = parseCommaList(line.slice(skip, labelSplit));
if(agents.length < 1 || (agents.length > 1 && !mode.multiAgent)) {
throw new Error('Invalid ' + line[0] + ': ' + line.join(' '));
}
return {
type: type.type,
agents,
mode: mode.mode,
label: line.slice(labelSplit + 1).join(' '),
};
}
function parseConnection(line) {
let labelSplit = line.indexOf(':');
if(labelSplit === -1) {
labelSplit = line.length;
}
let typeSplit = -1;
let options = null;
for(let j = 0; j < line.length; ++ j) {
const opts = CONNECTION_TYPES[line[j]];
if(opts) {
typeSplit = j;
options = opts;
break;
}
}
if(typeSplit <= 0 || typeSplit >= labelSplit - 1) {
return null;
}
return Object.assign({
type: 'connection',
agents: [
line.slice(0, typeSplit).join(' '),
line.slice(typeSplit + 1, labelSplit).join(' '),
],
label: line.slice(labelSplit + 1).join(' '),
}, options);
}
function parseMeta(line, meta) {
if(line[0] === 'title') {
meta.title = line.slice(1).join(' ');
return true;
}
if(line[0] === 'terminators') {
if(TERMINATOR_TYPES.indexOf(line[1]) === -1) {
throw new Error('Unrecognised termination: ' + line.join(' '));
}
meta.terminators = line[1];
return true;
}
return false;
}
function parseLine(line, {meta, stages}) {
if(parseMeta(line, meta)) {
return;
}
const stage = (
parseBlockCommand(line) ||
parseAgentCommand(line) ||
parseNote(line) ||
parseConnection(line)
);
if(!stage) {
throw new Error('Unrecognised command: ' + line.join(' '));
}
stages.push(stage);
}
return class Parser {
tokenise(src) {
const tokens = [];
let block = null;
let current = '';
for(let i = 0; i <= src.length;) {
const {newBlock, end, append, skip} = tokAdvance(src, i, block);
if(newBlock) {
block = newBlock;
current = '';
}
current += append;
i += skip;
if(end) {
if(!block.omit) {
tokens.push(current);
}
block = null;
}
}
if(block) {
throw new Error('Unterminated block');
}
return tokens;
}
splitLines(tokens) {
const lines = [];
let line = [];
tokens.forEach((token) => {
if(token === '\n') {
if(line.length > 0) {
lines.push(line);
line = [];
}
} else {
line.push(token);
}
});
if(line.length > 0) {
lines.push(line);
}
return lines;
}
parseLines(lines) {
const result = {
meta: {
title: '',
terminators: 'none',
},
stages: [],
};
lines.forEach((line) => parseLine(line, result));
return result;
}
parse(src) {
const tokens = this.tokenise(src);
const lines = this.splitLines(tokens);
return this.parseLines(lines);
}
};
});