import {combine, last} from '../../core/ArrayUtilities.mjs'; import Tokeniser from './Tokeniser.mjs'; import labelPatternParser from './LabelPatternParser.mjs'; import markdownParser from './MarkdownParser.mjs'; const BLOCK_TYPES = new Map(); BLOCK_TYPES.set('if', { blockType: 'if', skip: [], tag: 'if', type: 'block begin', }); BLOCK_TYPES.set('else', { blockType: 'else', skip: ['if'], tag: 'else', type: 'block split', }); BLOCK_TYPES.set('repeat', { blockType: 'repeat', skip: [], tag: 'repeat', type: 'block begin', }); BLOCK_TYPES.set('group', { blockType: 'group', skip: [], tag: '', type: 'block begin', }); const CONNECT = { agentFlags: { '!': {flag: 'end'}, '*': {allowBlankName: true, blankNameFlag: 'source', flag: 'begin'}, '+': {flag: 'start'}, '-': {flag: 'stop'}, }, types: ((() => { const lTypes = [ {tok: '', type: 0}, {tok: '<', type: 1}, {tok: '<<', type: 2}, {tok: '~', type: 3}, ]; const mTypes = [ {tok: '-', type: 'solid'}, {tok: '--', type: 'dash'}, {tok: '~', type: 'wave'}, ]; const rTypes = [ {tok: '', type: 0}, {tok: '>', type: 1}, {tok: '>>', type: 2}, {tok: '~', type: 3}, {tok: 'x', type: 4}, ]; const types = new Map(); combine([lTypes, mTypes, rTypes]).forEach((arrow) => { const [left, line, right] = arrow; if(left.type === 0 && right.type === 0) { // A line without arrows cannot be a connector return; } if(left.type === 3 && line.type === 'wave' && right.type === 0) { // ~~ could be fade-wave-none or none-wave-fade // We allow only none-wave-fade to resolve this return; } types.set(arrow.map((part) => part.tok).join(''), { left: left.type, line: line.type, right: right.type, }); }); return types; })()), }; const TERMINATOR_TYPES = [ 'none', 'box', 'cross', 'fade', 'bar', ]; const NOTE_TYPES = new Map(); NOTE_TYPES.set('text', { mode: 'text', types: { 'left': { max: Number.POSITIVE_INFINITY, min: 0, skip: ['of'], type: 'note left', }, 'right': { max: Number.POSITIVE_INFINITY, min: 0, skip: ['of'], type: 'note right', }, }, }); NOTE_TYPES.set('note', { mode: 'note', types: { 'between': { max: Number.POSITIVE_INFINITY, min: 2, skip: [], type: 'note between', }, 'left': { max: Number.POSITIVE_INFINITY, min: 0, skip: ['of'], type: 'note left', }, 'over': { max: Number.POSITIVE_INFINITY, min: 0, skip: [], type: 'note over', }, 'right': { max: Number.POSITIVE_INFINITY, min: 0, skip: ['of'], type: 'note right', }, }, }); NOTE_TYPES.set('state', { mode: 'state', types: { 'over': {max: 1, min: 1, skip: [], type: 'note over'}, }, }); const DIVIDER_TYPES = new Map(); DIVIDER_TYPES.set('line', {defaultHeight: 6}); DIVIDER_TYPES.set('space', {defaultHeight: 6}); DIVIDER_TYPES.set('delay', {defaultHeight: 30}); DIVIDER_TYPES.set('tear', {defaultHeight: 6}); const AGENT_MANIPULATION_TYPES = new Map(); AGENT_MANIPULATION_TYPES.set('define', {type: 'agent define'}); AGENT_MANIPULATION_TYPES.set('begin', {mode: 'box', type: 'agent begin'}); AGENT_MANIPULATION_TYPES.set('end', {mode: 'cross', type: 'agent end'}); function makeError(message, token = null) { let suffix = ''; if(token) { suffix = ( ' at line ' + (token.b.ln + 1) + ', character ' + token.b.ch ); } return new Error(message + suffix); } function endIndexInLine(line, end = null) { if(end === null) { return line.length; } return end; } function joinLabel(line, begin = 0, end = null) { const e = endIndexInLine(line, end); if(e <= begin) { return ''; } let result = line[begin].v; for(let i = begin + 1; i < e; ++ i) { result += line[i].s + line[i].v; } return result; } function readNumber(line, begin = 0, end = null, def = Number.NAN) { const text = joinLabel(line, begin, end); return Number(text || def); } function tokenKeyword(token) { if(!token || token.q) { return null; } return token.v; } function skipOver(line, start, skip, error = null) { for(let i = 0; i < skip.length; ++ i) { const expected = skip[i]; const token = line[start + i]; if(tokenKeyword(token) !== expected) { if(error) { throw makeError( error + '; expected "' + expected + '"', token ); } else { return start; } } } return start + skip.length; } function findTokens(line, tokens, { start = 0, limit = null, orEnd = false, } = {}) { const e = endIndexInLine(line, limit); for(let i = start; i <= e - tokens.length; ++ i) { if(skipOver(line, i, tokens) !== i) { return i; } } return orEnd ? e : -1; } function findFirstToken(line, tokenMap, {start = 0, limit = null} = {}) { const e = endIndexInLine(line, limit); for(let pos = start; pos < e; ++ pos) { const value = tokenMap.get(tokenKeyword(line[pos])); if(value) { return {pos, value}; } } return null; } function readAgentAlias(line, start, end, {enableAlias, allowBlankName}) { let aliasSep = end; if(enableAlias) { aliasSep = findTokens(line, ['as'], {limit: end, orEnd: true, start}); } if(start >= aliasSep && !allowBlankName) { let errPosToken = line[start]; if(!errPosToken) { errPosToken = {b: last(line).e}; } throw makeError('Missing agent name', errPosToken); } return { alias: joinLabel(line, aliasSep + 1, end), name: joinLabel(line, start, aliasSep), }; } function readAgent(line, start, end, { flagTypes = {}, aliases = false, } = {}) { const flags = []; const blankNameFlags = []; let p = start; let allowBlankName = false; for(; p < end; ++ p) { const token = line[p]; const rawFlag = tokenKeyword(token); const flag = flagTypes[rawFlag]; if(!flag) { break; } if(flags.includes(flag.flag)) { throw makeError('Duplicate agent flag: ' + rawFlag, token); } allowBlankName = allowBlankName || Boolean(flag.allowBlankName); flags.push(flag.flag); blankNameFlags.push(flag.blankNameFlag); } const {name, alias} = readAgentAlias(line, p, end, { allowBlankName, enableAlias: aliases, }); return { alias, flags: name ? flags : blankNameFlags, name, }; } function readAgentList(line, start, end, readAgentOpts) { const list = []; let currentStart = -1; for(let i = start; i < end; ++ i) { const token = line[i]; if(tokenKeyword(token) === ',') { if(currentStart !== -1) { list.push(readAgent(line, currentStart, i, readAgentOpts)); currentStart = -1; } } else if(currentStart === -1) { currentStart = i; } } if(currentStart !== -1) { list.push(readAgent(line, currentStart, end, readAgentOpts)); } return list; } const PARSERS = [ {begin: ['title'], fn: (line, meta) => { // Title meta.title = joinLabel(line, 1); return true; }}, {begin: ['theme'], fn: (line, meta) => { // Theme meta.theme = joinLabel(line, 1); return true; }}, {begin: ['terminators'], fn: (line, meta) => { // Terminators const type = tokenKeyword(line[1]); if(!type) { throw makeError('Unspecified termination', line[0]); } if(TERMINATOR_TYPES.indexOf(type) === -1) { throw makeError('Unknown termination "' + type + '"', line[1]); } meta.terminators = type; return true; }}, {begin: ['headers'], fn: (line, meta) => { // Headers const type = tokenKeyword(line[1]); if(!type) { throw makeError('Unspecified header', line[0]); } if(TERMINATOR_TYPES.indexOf(type) === -1) { throw makeError('Unknown header "' + type + '"', line[1]); } meta.headers = type; return true; }}, {begin: ['divider'], fn: (line) => { // Divider const labelSep = findTokens(line, [':'], {orEnd: true}); const heightSep = findTokens(line, ['with', 'height'], { limit: labelSep, orEnd: true, }); const mode = joinLabel(line, 1, heightSep) || 'line'; if(!DIVIDER_TYPES.has(mode)) { throw makeError('Unknown divider type', line[1]); } const height = readNumber( line, heightSep + 2, labelSep, DIVIDER_TYPES.get(mode).defaultHeight ); if(Number.isNaN(height) || height < 0) { throw makeError('Invalid divider height', line[heightSep + 2]); } return { height, label: joinLabel(line, labelSep + 1), mode, type: 'divider', }; }}, {begin: ['autolabel'], fn: (line) => { // Autolabel let raw = null; if(tokenKeyword(line[1]) === 'off') { raw = '