@bablr/language-en-cstml
Version:
A BABLR language for CSTML
472 lines (401 loc) • 12.4 kB
JavaScript
import { re, spam as m } from '@bablr/helpers/shorthand';
import {
Node,
CoveredBy,
InjectFrom,
Attributes,
UndefinedAttributes,
AllowEmpty,
Literal,
} from '@bablr/helpers/decorators';
import { o, eat, eatMatch, match, defineAttribute, fail } from '@bablr/helpers/grammar';
import { buildString } from '@bablr/helpers/builders';
import * as productions from '@bablr/helpers/productions';
import * as Space from '@bablr/language-en-blank-space';
import * as JSON from '@bablr/language-en-cstml-json';
export const canonicalURL = 'https://bablr.org/languages/core/en/cstml';
export const dependencies = { Space, JSON };
export const defaultMatcher = m`<Node />`;
const escapables = new Map(
Object.entries({
n: '\n',
r: '\r',
t: '\t',
0: '\0',
}),
);
function first(iter) {
for (let value of iter) return value;
}
export function* eatMatchTrivia() {
if (yield match(re`/[ \t\r\n]/`)) {
return yield eat(m`#: :Space: <__Blank />`);
}
return null;
}
export const grammar = class CSTMLGrammar {
*[Symbol.for('@bablr/fragment')]({ props: { rootMatcher } }) {
yield* eatMatchTrivia();
yield eat(rootMatcher);
yield* eatMatchTrivia();
}
*Stream() {
while (yield eatMatch(m`.[]: <_Tag />`));
}
*Expression() {
yield eat(m`<__Any />`, [m`<Document '<!' />`, m`<Node '<' />`]);
}
*Tag() {
yield eat(m`<__Any />`, [
m`<NullTag 'null' />`,
m`<InitializerTag /\[\]|undefined/ />`,
m`<AttributeDefinition '{' />`,
m`<ReferenceTag /[.#@a-zA-Z\u0060\u{80}-\u{10ffff}]/ />`,
m`<BindingTag ':' />`,
m`<LiteralTag /['"]/ />`,
m`<DoctypeTag '<!' />`,
m`<GapTag '<//>' />`,
m`<ShiftTag '^^^' />`,
m`<CloseNodeTag '</' />`,
m`<OpenNodeTag '<' />`,
]);
}
*Document() {
yield eatMatch(m`doctype$: <DoctypeTag '<!' />`, o({}), o({ bind: true }));
yield* eatMatchTrivia();
yield eat(m`tree$: <Node />`, o({ forceFragment: true }));
}
('Tag')
('Expression')
*DoctypeTag() {
yield eat(m`openToken: <*Punctuator '<!' { balancedSpan: 'Tag', balanced: '>' } />`);
yield eat(m`version$: :JSON: <*UnsignedInteger />`);
yield eat(m`versionSeparatorToken: <*Punctuator ':' />`);
yield eat(m`doctypeToken$: <*Keyword 'cstml' />`);
yield* eatMatchTrivia();
yield eat(m`closeToken: <*Punctuator '>' { balancer: true } />`);
}
('Tag')
('Expression')
*ReferenceTag({ ctx }) {
let type;
if ((type = yield match(re`/[.#@_]/`))) {
yield eat(m`type: <*Punctuator ${buildString(ctx.sourceTextFor(type))} />`);
} else {
yield eat(m`type: null`);
}
if (!type || ctx.sourceTextFor(type) === '#') {
if (type) {
yield eatMatch(m`name$: <Identifier />`, o({}), o({ bind: true }));
} else {
yield eat(m`name$: <Identifier />`);
}
} else {
yield eat(m`name$: null`, o({}), o({ bind: true }));
}
yield* eatMatchTrivia();
if (
yield eatMatch(
m`openIndexToken: <*Punctuator '[' { balanced: ']' } />`,
o({}),
o({ bind: true }),
)
) {
yield* eatMatchTrivia();
yield eat(m`closeIndexToken: <*Punctuator ']' { balancer: true } />`);
} else {
yield eat(m`closeIndexToken: null`);
}
yield* eatMatchTrivia();
yield eat(m`flags: <ReferenceFlags />`);
yield* eatMatchTrivia();
yield eat(m`sigilToken: <*Punctuator ':' />`);
}
*ReferenceFlags() {
yield eatMatch(m`expressionToken: <*Punctuator '+' />`, o({}), o({ bind: true }));
yield eatMatch(m`hasGapToken: <*Punctuator '$' />`, o({}), o({ bind: true }));
}
('Tag')
*BindingTag() {
yield eat(m`openToken: <*Punctuator ':' />`);
yield eatMatch(m`languagePath: <IdentifierPath />`);
yield eat(m`closeToken: <*Punctuator ':' />`);
}
('Tag')
('Expression')
('PropertyValue')
*GapTag() {
yield eat(m`sigilToken: <*Punctuator '<//>' />`);
}
('Tag')
*ShiftTag() {
yield eat(m`sigilToken: <*Punctuator '^^^' />`);
}
('Tag')
('Expression')
('PropertyValue')
*NullTag() {
yield eat(m`sigilToken: <*Keyword 'null' />`);
}
(['isArray'])
('Tag')
('PropertyValue')
*InitializerTag() {
let arr;
if ((arr = yield eatMatch(m`sigilToken: <*Punctuator '[]' />`))) {
} else if (yield eat(m`sigilToken: <*Keyword 'undefined' />`)) {
}
yield defineAttribute('isArray', !!arr);
}
('Tag')
*AttributeDefinition() {
yield eatMatch(m`openToken: <*Punctuator '{' { balanced: '}' } />`);
yield* eatMatchTrivia();
yield eat(m`key$: <IdentifierPath />`);
yield* eatMatchTrivia();
yield eat(m`sigilToken: <*Punctuator ':' />`);
yield* eatMatchTrivia();
yield eat(m`value$: :JSON: <_Expression />`);
yield* eatMatchTrivia();
yield eat(m`closeToken: <*Punctuator '}' { balancer: true } />`);
}
('PropertyValue')
*Node({ props }) {
let open = yield eat(m`open: <OpenNodeTag />`, o(props));
yield* eatMatchTrivia();
const flags = open.get('flags');
const balanced = open.attributes.balanced;
const token = !!flags?.get('tokenToken');
const selfClosing = !balanced;
yield eat(m`children[]$: []`);
if (selfClosing) {
yield eat(m`close: null`);
} else {
while ((yield match(re`/./s`)) && !(yield match('</'))) {
yield eat(m`children[]$: <_NodeChild />`, o({ token }));
yield* eatMatchTrivia();
}
yield eat(m`close: <CloseNodeTag />`);
}
}
*NodeChild({ props: { token } }) {
if (token) {
yield eat(m`<__Any />`, [
m`<AttributeDefinition '{' />`,
m`<Property '@' />`,
m`<LiteralTag /['"]/ />`,
]);
} else {
yield eat(m`<__Any />`, [m`<AttributeDefinition '{' />`, m`<Property /./s />`]);
}
}
('NodeChild')
*Property({ ctx }) {
let ref = null;
if (yield match('^^^')) {
yield eat(m`reference$: <ShiftTag />`);
} else {
ref = yield eatMatch(m`reference$: <ReferenceTag />`, o({}), o({ bind: true }));
}
yield* eatMatchTrivia();
yield eatMatch(m`binding$: <BindingTag ':' />`, o({}), o({ bind: true }));
yield* eatMatchTrivia();
yield eat(
m`value$: <_PropertyValue />`,
o({ allowFragment: ref && ctx.sourceTextFor(ref.get('type')) === '_' }),
);
}
*PropertyValue({ props: { allowFragment } }) {
yield eat(m`<__Any />`, [
m`<NullTag 'null' />`,
m`<GapTag '<//>' />`,
m`<InitializerTag /\[\]|undefined/ />`,
[m`<Node /\<[^#@]|['"]/ />`, o({ allowFragment, propertyValue: true })],
]);
}
*NodeFlags() {
yield eatMatch(m`tokenToken: <*Punctuator '*' />`, o({}), o({ bind: true }));
yield eatMatch(m`hasGapToken: <*Punctuator '$' />`, o({}), o({ bind: true }));
yield eatMatch(m`fragmentToken: <*Punctuator '_' />`, o({}), o({ bind: true }));
yield eatMatch(m`multiFragmentToken: <*Punctuator '_' />`, o({}), o({ bind: true }));
}
(['balanced', 'balancedSpan'])
('Tag')
*OpenNodeTag({
s,
props: { forceFragment = false, allowFragment = true, propertyValue = false },
}) {
const outerSpan = s.span;
if (yield match(re`/['"]/`)) {
yield eat(m`openToken: null`);
yield eat(m`flags: null`);
yield eat(m`type: null`);
yield eat(m`literalValue: :JSON: <String />`);
yield eat(m`attributes: null`);
yield eat(m`selfClosingTag: null`);
yield eat(m`closeToken: null`);
return;
}
yield eat(m`openToken: <*Punctuator '<' { balancedSpan: 'Tag', balanced: '>' } />`);
let flags = yield eat(m`flags: <NodeFlags />`);
let type = null;
let fragmentFlag = !!flags.get('fragmentToken');
let multiFragmentFlag = !!flags.get('multiFragmentToken');
if (forceFragment && !fragmentFlag) {
yield fail();
}
if (!allowFragment && fragmentFlag) {
yield fail();
}
if (propertyValue && fragmentFlag && !multiFragmentFlag) {
yield fail();
}
let isFragment = fragmentFlag;
if (isFragment) {
yield eat(m`type$: null`);
} else {
type = yield eat(m`type$: <Identifier />`);
}
if (!type && !fragmentFlag) {
yield fail();
}
let sp = type ? yield* eatMatchTrivia() : null;
if (type && sp && (yield match(re`/['"]/`))) {
yield eat(m`literalValue$: :JSON: <String />`);
sp = yield* eatMatchTrivia();
} else {
yield eat(m`literalValue$: null`);
}
if (type) {
yield eatMatch(m`attributes$: :JSON: <Object '{' />`, o({}), o({ bind: true }));
yield* eatMatchTrivia();
} else {
yield eat(m`attributes$: null`);
}
let sc;
if (!isFragment) {
sc = yield eatMatch(m`selfClosingToken: <*Punctuator '/' />`, o({}), o({ bind: true }));
} else {
sc = yield eat(m`selfClosingToken: null`);
}
const balanced = !sc && (s.depths.path > 0 || outerSpan !== 'Bare');
yield defineAttribute('balanced', balanced);
yield defineAttribute('balancedSpan', balanced ? 'NodeChildren' : null);
yield eat(m`closeToken: <*Punctuator '>' { balancer: true } />`);
}
({ balancer: true })
('Tag')
*CloseNodeTag() {
yield eat(m`openToken: <*Punctuator '</' { balanced: '>' } />`);
yield eat(m`closeToken: <*Punctuator '>' { balancer: true } />`);
}
*IdentifierPath() {
yield eat(m`segments[]$: <Identifier />`);
while (yield match('.')) {
yield eat(m`#separatorTokens[]: <*Punctuator '.' />`);
yield eat(m`segments[]$: <Identifier />`);
}
}
*Identifier() {
let q;
q = yield eatMatch(
m`openToken: <*Punctuator ${buildString('`')} { balanced: ${buildString('`')} } />`,
o({}),
o({ bind: true }),
);
yield eat(m`content: <*IdentifierContent { span: 'Identifier' } />`, o({ quoted: !!q }));
if (q) {
yield eat(
m`closeToken: <*Punctuator ${buildString('`')} { balancer: true } />`,
o({}),
o({ bind: true }),
);
} else {
yield eat(m`closeToken: null`);
}
}
*IdentifierContent({ props: { quoted = false } }) {
let lit, esc;
do {
if ((esc = yield match('\\'))) {
esc = yield eatMatch(m`@: <EscapeSequence />`);
} else {
if (!quoted) {
lit = yield eatMatch(re`/[a-zA-Z\u{80}-\u{10ffff}][a-zA-Z0-9_\u{80}-\u{10ffff}-]*/`);
} else {
lit = yield eatMatch(re`/[^\u0060\r\n]+/`);
}
}
} while (lit || esc);
}
('Tag')
('Expression')
('NodeChild')
*LiteralTag() {
yield eat(m`value: :JSON: <String />`);
}
(productions)
*Punctuator() {}
*EscapeSequence({ ctx, state: { span } }) {
if (!span.startsWith('Identifier')) {
yield fail();
}
yield eat(m`sigilToken: <*Punctuator '\\' { openSpan: 'Escape' } />`);
let codeNode = yield eat(m`code: <EscapeCode { closeSpan: 'Escape' } />`);
let cooked;
const type = ctx.sourceTextFor(codeNode.get('typeToken'));
const value = ctx.sourceTextFor(codeNode.get('value'));
if (span !== 'Identifier') {
throw new Error('not implemented');
}
if (type === 'u') {
cooked = String.fromCharCode(parseInt(value, 16));
} else {
throw new Error();
}
yield defineAttribute('cooked', cooked);
}
*EscapeCode() {
yield eat(m`typeToken: <*Keyword 'u' />`);
if (
yield eatMatch(m`openToken: <*Punctuator '{' { balanced: '}' } />`, o({}), o({ bind: true }))
) {
yield eat(m`value$: :JSON: <*UnsignedHexInteger />`);
yield eat(m`closeToken: <*Punctuator '}' { balancer: true } />`);
} else {
yield eat(m`value$: :JSON: <*UnsignedHexInteger /[\da-fA-F]{4}/ />`);
yield eat(m`closeToken: null`);
}
}
(productions)
*Keyword() {}
(productions)
*Any() {}
};