Files
puter/packages/phoenix/src/ansi-shell/parsing/buildParserFirstHalf.js
T
2024-04-12 20:53:44 -04:00

222 lines
8.6 KiB
JavaScript

/*
* Copyright (C) 2024 Puter Technologies Inc.
*
* This file is part of Phoenix Shell.
*
* Phoenix Shell is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { FirstRecognizedPStratumImpl, ParserBuilder, ParserFactory, StrUntilParserImpl, StrataParseFacade, WhitespaceParserImpl } from "strataparse";
import { UnquotedTokenParserImpl } from "./UnquotedTokenParserImpl.js";
import { PARSE_CONSTANTS } from "./PARSE_CONSTANTS.js";
import { MergeWhitespacePStratumImpl } from "strataparse/strata_impls/MergeWhitespacePStratumImpl.js";
import ContextSwitchingPStratumImpl from "strataparse/strata_impls/ContextSwitchingPStratumImpl.js";
const parserConfigProfiles = {
syntaxHighlighting: { cst: true },
interpreting: { cst: false }
};
const list_ws = [' ', '\n', '\t'];
const list_quot = [`"`, `'`];
const list_stoptoken = [
'|','>','<','&','\\','#',';','(',')',
...list_ws,
...list_quot
];
export const buildParserFirstHalf = (sp, profile) => {
const options = profile ? parserConfigProfiles[profile]
: { cst: false };
const parserFactory = new ParserFactory();
if ( options.cst ) {
parserFactory.concrete = true;
parserFactory.rememberSource = true;
}
const parserRegistry = StrataParseFacade.getDefaultParserRegistry();
const parserBuilder = new ParserBuilder({
parserFactory,
parserRegistry,
});
// TODO: unquoted tokens will actually need to be parsed in
// segments to because `$(echo "la")h` works in sh
const buildStringParserDef = quote => {
return a => a.sequence(
a.literal(quote),
a.repeat(a.choice(
// TODO: replace this with proper parser
parserFactory.create(StrUntilParserImpl, {
stopChars: ['\\', quote],
}, { assign: { $: 'string.segment' } }),
a.sequence(
a.literal('\\'),
a.choice(
a.literal(quote),
...Object.keys(
PARSE_CONSTANTS.escapeSubstitutions
).map(chr => a.literal(chr))
// TODO: \u[4],\x[2],\0[3]
)
).assign({ $: 'string.escape' })
)),
a.literal(quote),
).assign({ $: 'string' })
};
const buildStringContext = quote => [
parserFactory.create(StrUntilParserImpl, {
stopChars: ['\\', "$", quote],
}, { assign: { $: 'string.segment' } }),
parserBuilder.def(a => a.sequence(
a.literal('\\'),
a.choice(
a.literal(quote),
...Object.keys(
PARSE_CONSTANTS.escapeSubstitutions
).map(chr => a.literal(chr))
// TODO: \u[4],\x[2],\0[3]
)
).assign({ $: 'string.escape' })),
{
parser: parserBuilder.def(a => a.literal(quote).assign({ $: 'string.close' })),
transition: { pop: true }
},
{
parser: parserBuilder.def(a => {
return a.literal('$(').assign({ $: 'op.cmd-subst' })
}),
transition: {
to: 'command',
}
},
];
// sp.add(
// new FirstRecognizedPStratumImpl({
// parsers: [
// parserFactory.create(WhitespaceParserImpl),
// parserBuilder.def(a => a.literal('|').assign({ $: 'op.pipe' })),
// parserBuilder.def(a => a.literal('>').assign({ $: 'op.redirect', direction: 'out' })),
// parserBuilder.def(a => a.literal('<').assign({ $: 'op.redirect', direction: 'in' })),
// parserBuilder.def(a => a.literal('$((').assign({ $: 'op.arithmetic' })),
// parserBuilder.def(a => a.literal('$(').assign({ $: 'op.cmd-subst' })),
// parserBuilder.def(a => a.literal(')').assign({ $: 'op.close' })),
// parserFactory.create(StrUntilParserImpl, {
// stopChars: list_stoptoken,
// }, { assign: { $: 'symbol' } }),
// // parserFactory.create(UnquotedTokenParserImpl),
// parserBuilder.def(buildStringParserDef('"')),
// parserBuilder.def(buildStringParserDef(`'`)),
// ]
// })
// )
sp.add(
new ContextSwitchingPStratumImpl({
entry: 'command',
contexts: {
command: [
parserBuilder.def(a => a.literal('\n').assign({ $: 'op.line-terminator' })),
parserFactory.create(WhitespaceParserImpl),
parserBuilder.def(a => a.literal('|').assign({ $: 'op.pipe' })),
parserBuilder.def(a => a.literal('>').assign({ $: 'op.redirect', direction: 'out' })),
parserBuilder.def(a => a.literal('<').assign({ $: 'op.redirect', direction: 'in' })),
{
parser: parserBuilder.def(a => a.literal(')').assign({ $: 'op.close' })),
transition: {
pop: true,
}
},
{
parser: parserBuilder.def(a => a.literal('"').assign({ $: 'string.dquote' })),
transition: {
to: 'string.dquote',
}
},
{
parser: parserBuilder.def(a => a.literal(`'`).assign({ $: 'string.squote' })),
transition: {
to: 'string.squote',
}
},
{
parser: parserBuilder.def(a => a.none()),
transition: {
to: 'symbol',
}
},
],
'string.dquote': buildStringContext('"'),
'string.squote': buildStringContext(`'`),
symbol: [
parserFactory.create(StrUntilParserImpl, {
stopChars: [...list_stoptoken, '$'],
}, { assign: { $: 'symbol' } }),
{
// TODO: redundant definition to the one in 'command'
parser:
parserBuilder.def(a => a.literal('\n').assign({ $: 'op.line-terminator' })),
transition: { pop: true }
},
{
parser: parserFactory.create(WhitespaceParserImpl),
transition: { pop: true }
},
{
peek: true,
parser: parserBuilder.def(a => a.literal(')').assign({ $: 'op.close' })),
transition: { pop: true }
},
{
parser: parserBuilder.def(a => {
return a.literal('$(').assign({ $: 'op.cmd-subst' })
}),
transition: {
to: 'command',
}
},
{
parser: parserBuilder.def(a => a.none()),
transition: { pop: true }
},
{
parser: parserBuilder.def(a => a.choice(
...list_stoptoken.map(chr => a.literal(chr))
)),
transition: { pop: true }
}
],
},
wrappers: {
'string.dquote': {
$: 'string',
quote: '"',
},
'string.squote': {
$: 'string',
quote: `'`,
},
},
})
)
sp.add(
new MergeWhitespacePStratumImpl()
)
};