Skip to content

Commit

Permalink
Generate from angular selectors. ENG-7482
Browse files Browse the repository at this point in the history
  • Loading branch information
STRd6 committed Dec 10, 2024
1 parent 2ab73e1 commit 6888b88
Show file tree
Hide file tree
Showing 7 changed files with 362 additions and 5 deletions.
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"@babel/plugin-transform-react-jsx": "^7.13.12",
"@babel/preset-typescript": "^7.13.0",
"@builder.io/sdk": "^2.1.1",
"@danielx/hera": "^0.8.16",
"astring": "^1.8.6",
"csstype": "^3.0.4",
"fp-ts": "^2.11.10",
Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/__tests__/angular.selector.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { parse } from '../../src/generators/angular/selector-parser.js';

describe('Angular selectors', () => {
test('should parse gnarly selectors', () => {
expect(parse('ccc.c1#wat[co].c2[counter="cool"]#wat[x=\'y\'].c3')).toEqual({
tagName: 'ccc',
id: 'wat',
classes: ['c1', 'c2', 'c3'],
attributes: {
co: undefined,
counter: '"cool"',
x: "'y'",
},
});
});
});
51 changes: 46 additions & 5 deletions packages/core/src/generators/angular/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ import {
ToAngularOptions,
} from './types';

import { parse } from './selector-parser';

const { types } = babel;

const mappers: {
Expand Down Expand Up @@ -427,10 +429,49 @@ export const blockToAngular = ({

str += `</ng-container>`;
} else {
const elSelector = childComponents.find((impName) => impName === json.name)
? kebabCase(json.name)
: json.name;
str += `<${elSelector} `;
let tagName,
id,
classes = [],
attributes;

const isComponent = childComponents.find((impName) => impName === json.name);

if (isComponent) {
const selector = json.meta.selector || blockOptions?.selector;
if (selector) {
try {
({ tagName, id, classes, attributes } = parse(selector));
} catch {
tagName = kebabCase(json.name);
}
} else {
tagName = kebabCase(json.name);
}
} else {
tagName = json.name;
}

str += `<${tagName} `;

if (id) {
str += `#${id} `;
}

// TODO: merge with existing classes/bindings
if (classes.length) {
str += `class="${classes.join(' ')}" `;
}

// TODO: Merge with existing properties
if (attributes) {
Object.entries(attributes).forEach(([key, value]) => {
if (value) {
str += `${key}=${value} `;
} else {
str += `${key} `;
}
});
}

for (const key in json.properties) {
if (key.startsWith('$')) {
Expand Down Expand Up @@ -516,7 +557,7 @@ export const blockToAngular = ({
.join('\n');
}

str += `</${elSelector}>`;
str += `</${tagName}>`;
}
return str;
};
Expand Down
67 changes: 67 additions & 0 deletions packages/core/src/generators/angular/selector-parser.hera
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Parser for angular selectors
# https://angular.dev/guide/components/selectors#types-of-selectors
#
# to build:
# ./node_modules/.bin/hera --libPath @danielx/hera/dist/machine.js < src/generators/angular/selector-parser.hera > src/generators/angular/selector-parser.js
Selector
TagName?:tagName Part*:parts ->
const firstId = parts.find(([key]) => key === "id");
let id;
if (firstId) {
id = firstId[1];
}

return {
tagName,
id,
classes: parts.filter(([key]) => key === "class").map(([, value]) => value),
attributes: Object.fromEntries(parts.filter(([key]) => key !== "id" && key !== "class"))
}

TagName
Identifier

Part
Attribute
Class
Id

Attribute
"[" __ AttributeName __ "=" __ AttributeValue __ "]" ->
return [$3, $7]
"[" __ AttributeName __ "]" ->
return [$3]

AttributeValue
$DoubleQuotedString
$SingleQuotedString

DoubleQuotedString
"\"" /(?:\\.|[^"])*/ "\""

SingleQuotedString
"'" /(?:\\.|[^'])*/ "'"

Class
"." Identifier ->
return ["class", $2]

Id
"#" Identifier ->
return ["id", $2]

EscapeSequence
"'"
"\""
"\\"
/./ ->
return "\\" + $0

Identifier
/[_a-zA-Z][_a-zA-Z0-9-]*/ -> $0

AttributeName
Identifier

__
/[ \t]*/
221 changes: 221 additions & 0 deletions packages/core/src/generators/angular/selector-parser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
const {
$C,
$E,
$EVENT,
$EVENT_C,
$EXPECT,
$L,
$N,
$P,
$Q,
$R,
$R$0,
$S,
$T,
$TEXT,
$TR,
$TS,
$TV,
$Y,
ParseError,
Parser,
Validator
} = require("@danielx/hera/dist/machine.js")


const grammar = {
Selector: Selector,
TagName: TagName,
Part: Part,
Attribute: Attribute,
AttributeValue: AttributeValue,
DoubleQuotedString: DoubleQuotedString,
SingleQuotedString: SingleQuotedString,
Class: Class,
Id: Id,
EscapeSequence: EscapeSequence,
Identifier: Identifier,
AttributeName: AttributeName,
__: __
};

const $L0 = $L("[");
const $L1 = $L("=");
const $L2 = $L("]");
const $L3 = $L("\"");
const $L4 = $L("'");
const $L5 = $L(".");
const $L6 = $L("#");
const $L7 = $L("\\");


const $R0 = $R(new RegExp("(?:\\\\.|[^\"])*", 'suy'));
const $R1 = $R(new RegExp("(?:\\\\.|[^'])*", 'suy'));
const $R2 = $R(new RegExp(".", 'suy'));
const $R3 = $R(new RegExp("[_a-zA-Z][_a-zA-Z0-9-]*", 'suy'));
const $R4 = $R(new RegExp("[ \\t]*", 'suy'));


//@ts-ignore
const Selector$0 = $TS($S($E(TagName), $Q(Part)), function($skip, $loc, $0, $1, $2) {
var tagName = $1;var parts = $2;
const firstId = parts.find(([key]) => key === "id");
let id;
if (firstId) {
id = firstId[1];
}

return {
tagName,
id,
classes: parts.filter(([key]) => key === "class").map(([, value]) => value),
attributes: Object.fromEntries(parts.filter(([key]) => key !== "id" && key !== "class"))
}
});
//@ts-ignore
function Selector(ctx, state) { return $EVENT(ctx, state, "Selector", Selector$0) }

//@ts-ignore
const TagName$0 = Identifier
//@ts-ignore
function TagName(ctx, state) { return $EVENT(ctx, state, "TagName", TagName$0) }

//@ts-ignore
const Part$0 = Attribute
//@ts-ignore
const Part$1 = Class
//@ts-ignore
const Part$2 = Id
//@ts-ignore
const Part$$ = [Part$0,Part$1,Part$2]
//@ts-ignore
function Part(ctx, state) { return $EVENT_C(ctx, state, "Part", Part$$) }

//@ts-ignore
const Attribute$0 = $TS($S($EXPECT($L0, "Attribute \"[\""), __, AttributeName, __, $EXPECT($L1, "Attribute \"=\""), __, AttributeValue, __, $EXPECT($L2, "Attribute \"]\"")), function($skip, $loc, $0, $1, $2, $3, $4, $5, $6, $7, $8, $9) {

return [$3, $7]
});
//@ts-ignore
const Attribute$1 = $TS($S($EXPECT($L0, "Attribute \"[\""), __, AttributeName, __, $EXPECT($L2, "Attribute \"]\"")), function($skip, $loc, $0, $1, $2, $3, $4, $5) {

return [$3]
});
//@ts-ignore
const Attribute$$ = [Attribute$0,Attribute$1]
//@ts-ignore
function Attribute(ctx, state) { return $EVENT_C(ctx, state, "Attribute", Attribute$$) }

//@ts-ignore
const AttributeValue$0 = $TEXT(DoubleQuotedString)
//@ts-ignore
const AttributeValue$1 = $TEXT(SingleQuotedString)
//@ts-ignore
const AttributeValue$$ = [AttributeValue$0,AttributeValue$1]
//@ts-ignore
function AttributeValue(ctx, state) { return $EVENT_C(ctx, state, "AttributeValue", AttributeValue$$) }

//@ts-ignore
const DoubleQuotedString$0 = $S($EXPECT($L3, "DoubleQuotedString \"\\\\\\\"\""), $R$0($EXPECT($R0, "DoubleQuotedString /(?:\\\\.|[^\"])*/")), $EXPECT($L3, "DoubleQuotedString \"\\\\\\\"\""))
//@ts-ignore
function DoubleQuotedString(ctx, state) { return $EVENT(ctx, state, "DoubleQuotedString", DoubleQuotedString$0) }

//@ts-ignore
const SingleQuotedString$0 = $S($EXPECT($L4, "SingleQuotedString \"'\""), $R$0($EXPECT($R1, "SingleQuotedString /(?:\\\\.|[^'])*/")), $EXPECT($L4, "SingleQuotedString \"'\""))
//@ts-ignore
function SingleQuotedString(ctx, state) { return $EVENT(ctx, state, "SingleQuotedString", SingleQuotedString$0) }

//@ts-ignore
const Class$0 = $TS($S($EXPECT($L5, "Class \".\""), Identifier), function($skip, $loc, $0, $1, $2) {

return ["class", $2]
});
//@ts-ignore
function Class(ctx, state) { return $EVENT(ctx, state, "Class", Class$0) }

//@ts-ignore
const Id$0 = $TS($S($EXPECT($L6, "Id \"#\""), Identifier), function($skip, $loc, $0, $1, $2) {

return ["id", $2]
});
//@ts-ignore
function Id(ctx, state) { return $EVENT(ctx, state, "Id", Id$0) }

//@ts-ignore
const EscapeSequence$0 = $EXPECT($L4, "EscapeSequence \"'\"")
//@ts-ignore
const EscapeSequence$1 = $EXPECT($L3, "EscapeSequence \"\\\\\\\"\"")
//@ts-ignore
const EscapeSequence$2 = $EXPECT($L7, "EscapeSequence \"\\\\\\\\\"")
//@ts-ignore
const EscapeSequence$3 = $TR($EXPECT($R2, "EscapeSequence /./"), function($skip, $loc, $0, $1, $2, $3, $4, $5, $6, $7, $8, $9) {
return "\\" + $0
});
//@ts-ignore
const EscapeSequence$$ = [EscapeSequence$0,EscapeSequence$1,EscapeSequence$2,EscapeSequence$3]
//@ts-ignore
function EscapeSequence(ctx, state) { return $EVENT_C(ctx, state, "EscapeSequence", EscapeSequence$$) }

//@ts-ignore
const Identifier$0 = $T($EXPECT($R3, "Identifier /[_a-zA-Z][_a-zA-Z0-9-]*/"), function(value) { return value[0] });
//@ts-ignore
function Identifier(ctx, state) { return $EVENT(ctx, state, "Identifier", Identifier$0) }

//@ts-ignore
const AttributeName$0 = Identifier
//@ts-ignore
function AttributeName(ctx, state) { return $EVENT(ctx, state, "AttributeName", AttributeName$0) }

//@ts-ignore
const __$0 = $R$0($EXPECT($R4, "__ /[ \\t]*/"))
//@ts-ignore
function __(ctx, state) { return $EVENT(ctx, state, "__", __$0) }



const parser = (function() {
const { fail, validate, reset } = Validator()
let ctx = { expectation: "", fail }

return {
parse: (input, options = {}) => {
if (typeof input !== "string") throw new Error("Input must be a string")

const parser = (options.startRule != null)
? grammar[options.startRule]
: Object.values(grammar)[0]

if (!parser) throw new Error(`Could not find rule with name '${options.startRule}'`)

const filename = options.filename || "<anonymous>";

reset()
Object.assign(ctx, { ...options.events, tokenize: options.tokenize });

return validate(input, parser(ctx, {
input,
pos: 0,
}), {
filename: filename
})
}
}
}())

exports.default = parser
const parse = exports.parse = parser.parse

exports.Selector = Selector;
exports.TagName = TagName;
exports.Part = Part;
exports.Attribute = Attribute;
exports.AttributeValue = AttributeValue;
exports.DoubleQuotedString = DoubleQuotedString;
exports.SingleQuotedString = SingleQuotedString;
exports.Class = Class;
exports.Id = Id;
exports.EscapeSequence = EscapeSequence;
exports.Identifier = Identifier;
exports.AttributeName = AttributeName;
exports.__ = __;

1 change: 1 addition & 0 deletions packages/core/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"compilerOptions": {
"lib": ["es7", "dom", "esnext", "esnext.asynciterable"],
"allowJs": true,
"composite": true,
"outDir": "dist",
"strict": true,
Expand Down
Loading

0 comments on commit 6888b88

Please sign in to comment.