Skip to content

Instantly share code, notes, and snippets.

@kidqueb
Last active January 26, 2022 22:58
Show Gist options
  • Save kidqueb/e7e49ad494570039d271d913e9294706 to your computer and use it in GitHub Desktop.
Save kidqueb/e7e49ad494570039d271d913e9294706 to your computer and use it in GitHub Desktop.
asd
import { SchemaFormElements, SchemaFormProperties, SchemaFormType } from 'jtd';
import { get, set } from 'lodash';
import { Merge } from 'type-fest';
const RE_TAG = /\{+\s*(.*?)\s*\}+/g;
const RE_THIS = /this.?/;
const RE_WITH = /#with\s+/;
const RE_EACH = /#each\s+/;
const RE_OPTIONAL = /#(if|unless)\s+/;
type Schema = Merge<Merge<SchemaFormType, SchemaFormElements>, SchemaFormProperties>;
type Type = 'string' | 'string[]' | 'object[]' | 'object';
type Segment = [type: Type, optional: boolean];
type AST = Record<string, Segment>;
export function generate(tpl: string) {
const tokens = _tokenize(tpl);
const ast = _parse(tokens);
return _build(ast);
}
function _tokenize(tpl: string) {
let tags = [];
let matches;
while ((matches = RE_TAG.exec(tpl))) {
if (matches) {
tags.push(matches[1]);
}
}
return tags;
}
function _parse(tokens: string[]) {
const ast: AST = {};
const context: string[] = [];
function setKey(key: string, type: Type, optional: boolean) {
const existing = ast[key];
if (!existing) {
ast[key] = [type, optional];
return;
}
let t = existing[0];
let o = optional || existing[1];
if (
t === 'string' ||
(t === 'object' && type === 'object[]') ||
(t === 'object[]' && type === 'string[]')
) {
ast[key] = [type, o];
}
}
for (let index = 0; index < tokens.length; index++) {
let token = tokens[index];
if (token.startsWith('/each')) {
context.pop();
continue;
}
if (token === 'else' || token.startsWith('/') || token.startsWith('! ')) {
continue;
}
let type: Type = 'string';
let optional = false;
if (token.startsWith('#if ') || token.startsWith('#unless ')) {
token = token.replace(RE_OPTIONAL, '');
optional = true;
}
if (token.startsWith('#with ')) {
token = token.replace(RE_WITH, '');
type = 'object';
}
if (token.startsWith('#each ')) {
token = token.replace(RE_EACH, '').replace(RE_THIS, '');
context.push(token);
type = 'object[]';
setKey(context.join('.'), 'object[]', false);
continue;
}
if (token === 'this') {
setKey(context.join('.'), 'string[]', false);
continue;
}
if (token.includes('.')) {
let key = token.startsWith('this') ? context.join('.') : '';
const keys = token.replace(RE_THIS, '').split('.');
for (let index = 0; index < keys.length; index++) {
key = key ? `${key}.${keys[index]}` : keys[index];
const type = index === keys.length - 1 ? 'string' : 'object';
setKey(key, type, optional);
}
continue;
}
setKey(token, type, optional);
}
return ast;
}
function _build(ast: AST): Schema {
const schema = {} as Schema;
const astKeys = Object.keys(ast).sort((a, b) => a.localeCompare(b));
for (const astKey of astKeys) {
const segments = astKey.split('.');
const processed: string[] = [];
const workingPath: string[] = [];
const process = (segment: string) => {
const prevPath = processed.join('.');
const prev = ast[prevPath] ?? ['', false];
processed.push(segment);
const checkPath = processed.join('.');
const astItem = ast[checkPath] ?? ['object', false];
let value = {};
const [type, optional] = astItem;
const [prevType] = prev;
if (!workingPath.length || prevType === 'object' || prevType === 'object[]') {
workingPath.push(optional ? 'optionalProperties' : 'properties');
}
workingPath.push(segment);
if (type === 'string[]') {
workingPath.push('elements');
}
if (type === 'object[]') {
workingPath.push('elements');
}
// Make the path, and if we havent already, add it to the schema
const path = workingPath.join('.');
if (get(schema, path)) return;
set(schema, path, value);
};
for (const segment of segments) {
process(segment);
}
}
return schema;
}
import { describe, expect, test } from 'vitest';
import { generate } from './v3';
describe('generate', () => {
test('wip', () => {
let schema = generate(`
{{firstname}} {{lastname}}
{{#if person}}
{{person.firstname}} {{person.lastname}}
{{/if}}
{{#each categories}}
{{this}}
{{/each}}
{{#each person.pet}}
{{this.name}}
{{#if this.breed}}
{{this.breed}}
{{/if}}
{{/each}}
{{#each user.vehicle}}
{{this.make}}
{{#each this.wheels}}
{{this.size}}
{{this.brand}}
{{/each}}
{{/each}}
{{#if pet}}
{{pet.age}}
{{#if pet.breed}}
{{pet.breed.name}}
{{#if pet.breed.region}}
{{pet.breed.region}}
{{/if}}
{{/if}}
{{pet.name}}
{{/if}}
{{#unless kid.active}}
{{kid.age}}
{{#unless kid.title}}
{{kid.title}}
{{/unless}}
{{kid.name}}
{{/unless}}
`);
expect(schema).toMatchInlineSnapshot(`
{
"optionalProperties": {
"kid": {
"optionalProperties": {
"active": {},
"title": {},
},
"properties": {
"age": {},
"name": {},
},
},
"person": {
"properties": {
"firstname": {},
"lastname": {},
"pet": {
"elements": {
"optionalProperties": {
"breed": {},
},
"properties": {
"name": {},
},
},
},
},
},
"pet": {
"optionalProperties": {
"breed": {
"optionalProperties": {
"region": {},
},
"properties": {
"name": {},
},
},
},
"properties": {
"age": {},
"name": {},
},
},
},
"properties": {
"categories": {
"elements": {},
},
"firstname": {},
"lastname": {},
"user": {
"vehicle": {
"elements": {
"properties": {
"make": {},
"wheels": {
"elements": {
"properties": {
"brand": {},
"size": {},
},
},
},
},
},
},
},
},
}
`);
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment