@table-bro/mcp 0.0.0-stage → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -2
  3. package/dist/docs/docs.d.ts +21 -0
  4. package/dist/docs/docs.js +103 -0
  5. package/dist/docs/pages.d.ts +28 -0
  6. package/dist/docs/pages.js +2734 -0
  7. package/dist/docs/topics.d.ts +11 -0
  8. package/dist/docs/topics.js +410 -0
  9. package/dist/extension.d.ts +23 -0
  10. package/dist/extension.js +1 -0
  11. package/dist/generate/angular.d.ts +26 -0
  12. package/dist/generate/angular.js +241 -0
  13. package/dist/generate/cells.d.ts +22 -0
  14. package/dist/generate/cells.js +157 -0
  15. package/dist/generate/code.d.ts +48 -0
  16. package/dist/generate/code.js +153 -0
  17. package/dist/generate/config.d.ts +16 -0
  18. package/dist/generate/config.js +95 -0
  19. package/dist/generate/fillings.d.ts +49 -0
  20. package/dist/generate/fillings.js +130 -0
  21. package/dist/generate/filters.d.ts +36 -0
  22. package/dist/generate/filters.js +254 -0
  23. package/dist/generate/generate-code.d.ts +34 -0
  24. package/dist/generate/generate-code.js +59 -0
  25. package/dist/generate/names.d.ts +52 -0
  26. package/dist/generate/names.js +93 -0
  27. package/dist/generate/rows.d.ts +5 -0
  28. package/dist/generate/rows.js +22 -0
  29. package/dist/generate/scaffold.d.ts +35 -0
  30. package/dist/generate/scaffold.js +62 -0
  31. package/dist/generate/service.d.ts +16 -0
  32. package/dist/generate/service.js +320 -0
  33. package/dist/generate/setup.d.ts +34 -0
  34. package/dist/generate/setup.js +158 -0
  35. package/dist/generate/vanilla.d.ts +10 -0
  36. package/dist/generate/vanilla.js +160 -0
  37. package/dist/index.d.ts +14 -0
  38. package/dist/index.js +10 -0
  39. package/dist/project/inspect-project.d.ts +53 -0
  40. package/dist/project/inspect-project.js +223 -0
  41. package/dist/server/main.d.ts +2 -0
  42. package/dist/server/main.js +8 -0
  43. package/dist/server/server.d.ts +17 -0
  44. package/dist/server/server.js +212 -0
  45. package/dist/spec/builtin-specs.d.ts +13 -0
  46. package/dist/spec/builtin-specs.js +167 -0
  47. package/dist/spec/css-variables.d.ts +22 -0
  48. package/dist/spec/css-variables.js +54 -0
  49. package/dist/spec/schema.d.ts +19 -0
  50. package/dist/spec/schema.js +278 -0
  51. package/dist/spec/skin-config.d.ts +30 -0
  52. package/dist/spec/skin-config.js +67 -0
  53. package/dist/spec/spec.d.ts +157 -0
  54. package/dist/spec/spec.js +2 -0
  55. package/dist/spec/validate-spec.d.ts +54 -0
  56. package/dist/spec/validate-spec.js +979 -0
  57. package/dist/version.d.ts +2 -0
  58. package/dist/version.js +2 -0
  59. package/package.json +71 -4
@@ -0,0 +1,95 @@
1
+ import { skinCallbacks } from '../spec/skin-config.js';
2
+ import { indentBlock, isPlainObject, lineComment, literal, raw } from './code.js';
3
+ import { BroFiltersCode } from './filters.js';
4
+ import { BroNames } from './names.js';
5
+ /**
6
+ * The config of the table from the spec: the functions and the settings of the skin come from the data of the spec,
7
+ * only the bodies of the callbacks and of the custom rules from a model.
8
+ */
9
+ export function configCode(spec, skin, fillings, indent, tagSuffix = '', extensions = []) {
10
+ const names = BroNames.of(spec, tagSuffix);
11
+ const config = spec.config ?? {};
12
+ const result = { code: '', preamble: [], imports: { '@table-bro/core': ['type BroConfig'] } };
13
+ const hasFilters = Boolean(spec.filters?.length);
14
+ const value = {
15
+ skin,
16
+ toolbarTitle: config.toolbarTitle,
17
+ pageSize: config.pageSize,
18
+ server: config.server,
19
+ rowCounter: config.rowCounter,
20
+ shadowsEnabled: config.shadowsEnabled,
21
+ theme: config.theme,
22
+ rowDetailsComponent: config.rowDetailsComponent && names.tag(config.rowDetailsComponent.cell),
23
+ rowDetailsServer: config.rowDetailsServer,
24
+ rowDetailsMotion: config.rowDetailsMotion,
25
+ // The fields of the paid modules are data: the generator writes them as they are
26
+ ...Object.fromEntries(extensions.flatMap(extension => Object.keys(extension.config ?? {})).map(field => [field, config[field]]))
27
+ };
28
+ const shortTitles = spec.cols.filter(col => col.shortTitle).map(col => [col.title, col.shortTitle]);
29
+ if (shortTitles.length) {
30
+ result.preamble.push(`/** The short titles of the columns: by them the table knows how narrow a column may be. */\nconst shortTitles: Record<string, string> = ${literal(Object.fromEntries(shortTitles))};`);
31
+ result.imports['@table-bro/core'].push('type BroCol');
32
+ value['shortColumnTitle'] = raw(`(col: BroCol<${spec.rowType}>) => shortTitles[col.title] ?? col.title?.split(' ')[0]`);
33
+ }
34
+ // The server applies the filters in the server mode
35
+ if (hasFilters && !config.server) {
36
+ result.imports['@table-bro/core'].push('matchesFilterCondition');
37
+ value['filtersTransform'] = raw(new BroFiltersCode(spec, fillings).transformCode());
38
+ }
39
+ const components = merge({}, spec.skinConfig?.[skin]?.components ?? {});
40
+ // The toolbar of a skin has only its search: a table with filters gets the Filters button, which opens the drawer
41
+ if (hasFilters) {
42
+ result.imports['@table-bro/core'].push('events');
43
+ }
44
+ merge(components, { toolbarMain: { buttons: toolbarButtons(spec, skin, hasFilters) } });
45
+ // The messages of the states go into the group state of the texts: the icons stay the ones of the skin, a font icon,
46
+ // an emoji or an animation of Lottie, which the page may give the skin
47
+ const states = Object.entries(spec.stateMessages ?? {});
48
+ if (states.length) {
49
+ value['texts'] = { state: Object.fromEntries(states) };
50
+ }
51
+ for (const callback of spec.skinConfig?.[skin]?.callbacks ?? []) {
52
+ const known = skinCallbacks.find(item => item.path === callback.path);
53
+ const body = fillings.callbacks?.[skin]?.[callback.path]?.trim() || `// TODO: ${lineComment(callback.does)}`;
54
+ const [, parameters, returns] = /^(\(.*?\)) => (.*)$/.exec(known.signature);
55
+ const path = callback.path.split('.').slice(1);
56
+ merge(components, path.reduceRight((item, key) => ({ [key]: item }), raw(`${parameters}: ${returns} => {\n${indentBlock(body, ' ')}\n}`)));
57
+ }
58
+ value['components'] = components;
59
+ value['css'] = spec.skinConfig?.[skin]?.css;
60
+ result.code = literal(value, indent);
61
+ return result;
62
+ }
63
+ function toolbarButtons(spec, skin, hasFilters) {
64
+ const material = (skin === 'mat');
65
+ const button = ({ title, action, icon }) => material ? { buttonTitle: title, iconHTML: icon, eventType: action, buttonTooltip: title } : { buttonTitle: title, eventType: action };
66
+ const buttons = [];
67
+ if (hasFilters) {
68
+ const eventType = raw('events.filtersToggleEvent');
69
+ buttons.push(material ? { buttonTitle: 'Filters', iconHTML: 'filter_list', eventType, buttonTooltip: 'Filters' } : { buttonTitle: 'Filters', eventType });
70
+ }
71
+ buttons.push(...(spec.toolbar?.buttons ?? []).map(button));
72
+ const menu = spec.toolbar?.menu;
73
+ if (menu) {
74
+ buttons.push({
75
+ buttonTitle: menu.title,
76
+ mode: 'navbar-dropdown',
77
+ buttonTooltip: material ? menu.title : undefined,
78
+ options: menu.items.map(({ title, action, icon }) => ({ value: action, iconHTML: material ? icon : undefined, title }))
79
+ });
80
+ }
81
+ return buttons;
82
+ }
83
+ /** Merges plain objects as the table merges the config of the application over the config of the skin. */
84
+ function merge(target, source) {
85
+ for (const [key, item] of Object.entries(source)) {
86
+ const current = target[key];
87
+ if (isPlainObject(item) && isPlainObject(current)) {
88
+ merge(current, item);
89
+ }
90
+ else {
91
+ target[key] = isPlainObject(item) ? merge({}, item) : item;
92
+ }
93
+ }
94
+ return target;
95
+ }
@@ -0,0 +1,49 @@
1
+ import type { BroSpecSkin } from '../spec/spec.js';
2
+ import type { BroSpecIssue } from '../spec/validate-spec.js';
3
+ /**
4
+ * The parts of the code that a model writes, by the contracts of the spec: the generator puts them into its scaffolds,
5
+ * which are always the same and right.
6
+ */
7
+ export interface BroSpecFillings {
8
+ /** The cells and the row details by their names, for each skin. */
9
+ cells?: Record<string, Partial<Record<BroSpecSkin, BroCellFilling>>>;
10
+ /** The conditions of the filters with the match custom, by their names: the body of (row, value) => boolean. */
11
+ filterRules?: Record<string, string>;
12
+ /** The bodies of the functions of the skin config, by the skin and the path, as components.sortHeader.afterRender. */
13
+ callbacks?: Partial<Record<BroSpecSkin, Record<string, string>>>;
14
+ }
15
+ /**
16
+ * The errors of the form of fillings, each with its path: a part of another type would fail the generator. The parts
17
+ * themselves are code, which the generator does not check; null is no part.
18
+ */
19
+ export declare function fillingsErrors(fillings: unknown): BroSpecIssue[];
20
+ /** The errors of the form of the filling of one cell or of the row details. */
21
+ export declare function fillingErrors(filling: unknown, path: string): BroSpecIssue[];
22
+ export interface BroCellFilling {
23
+ /**
24
+ * Angular: the components of the project the template uses, as {name, from}. The classes of Angular Material and
25
+ * the pipes of Angular come from the template itself (templateClasses), so a model writes no imports.
26
+ */
27
+ imports?: (string | {
28
+ name: string;
29
+ from: string;
30
+ })[];
31
+ /** Angular: the template of the component, in every skin. */
32
+ template?: string;
33
+ /** Angular: the styles of the component. */
34
+ styles?: string;
35
+ /** Vanilla JS: the body of render(), which builds the DOM of the cell. */
36
+ render?: string;
37
+ /** Members of the class: computed values, handlers. */
38
+ members?: string;
39
+ }
40
+ /**
41
+ * The classes a generated component of the mat skin imports by name. The classes, not their NgModules: on the site a
42
+ * component compiled in the browser finds nothing through an NgModule of a library.
43
+ */
44
+ export declare const angularClasses: Record<string, string>;
45
+ /**
46
+ * The classes of angularClasses that a template uses, found by their selectors and pipes: the imports of a generated
47
+ * component come from its template, so a model writes no imports and no NgModule gets into them.
48
+ */
49
+ export declare function templateClasses(template: string): string[];
@@ -0,0 +1,130 @@
1
+ import { isPlainObject } from './code.js';
2
+ /**
3
+ * The errors of the form of fillings, each with its path: a part of another type would fail the generator. The parts
4
+ * themselves are code, which the generator does not check; null is no part.
5
+ */
6
+ export function fillingsErrors(fillings) {
7
+ const errors = [];
8
+ if (!objectAt(fillings, 'fillings', errors)) {
9
+ return errors;
10
+ }
11
+ if (objectAt(fillings['cells'], 'fillings.cells', errors)) {
12
+ for (const [cell, skins] of Object.entries(fillings['cells'])) {
13
+ if (objectAt(skins, `fillings.cells.${cell}`, errors)) {
14
+ for (const [skin, filling] of Object.entries(skins)) {
15
+ errors.push(...fillingErrors(filling, `fillings.cells.${cell}.${skin}`));
16
+ }
17
+ }
18
+ }
19
+ }
20
+ if (objectAt(fillings['filterRules'], 'fillings.filterRules', errors)) {
21
+ for (const [filter, body] of Object.entries(fillings['filterRules'])) {
22
+ stringAt(body, `fillings.filterRules.${filter}`, errors);
23
+ }
24
+ }
25
+ if (objectAt(fillings['callbacks'], 'fillings.callbacks', errors)) {
26
+ for (const [skin, bodies] of Object.entries(fillings['callbacks'])) {
27
+ if (objectAt(bodies, `fillings.callbacks.${skin}`, errors)) {
28
+ for (const [path, body] of Object.entries(bodies)) {
29
+ stringAt(body, `fillings.callbacks.${skin}.${path}`, errors);
30
+ }
31
+ }
32
+ }
33
+ }
34
+ return errors;
35
+ }
36
+ /** The errors of the form of the filling of one cell or of the row details. */
37
+ export function fillingErrors(filling, path) {
38
+ const errors = [];
39
+ if (objectAt(filling, path, errors)) {
40
+ for (const part of ['template', 'styles', 'render', 'members']) {
41
+ stringAt(filling[part], `${path}.${part}`, errors);
42
+ }
43
+ if (filling['imports'] !== undefined && filling['imports'] !== null && !Array.isArray(filling['imports'])) {
44
+ errors.push({ path: `${path}.imports`, message: 'expected an array', fix: 'see get_docs with the topic fillings' });
45
+ }
46
+ }
47
+ return errors;
48
+ }
49
+ /** Whether a part is an object; a part of another type is an error, an absent one is none. */
50
+ function objectAt(value, path, errors) {
51
+ if (value === undefined || value === null) {
52
+ return false;
53
+ }
54
+ if (!isPlainObject(value)) {
55
+ errors.push({ path, message: 'expected an object', fix: 'see get_docs with the topic fillings' });
56
+ return false;
57
+ }
58
+ return true;
59
+ }
60
+ function stringAt(value, path, errors) {
61
+ if (value !== undefined && value !== null && typeof value !== 'string') {
62
+ errors.push({ path, message: 'expected the code as a string', fix: 'see get_docs with the topic fillings' });
63
+ }
64
+ }
65
+ /**
66
+ * The classes a generated component of the mat skin imports by name. The classes, not their NgModules: on the site a
67
+ * component compiled in the browser finds nothing through an NgModule of a library.
68
+ */
69
+ export const angularClasses = {
70
+ 'CurrencyPipe': '@angular/common',
71
+ 'DatePipe': '@angular/common',
72
+ 'DecimalPipe': '@angular/common',
73
+ 'LowerCasePipe': '@angular/common',
74
+ 'PercentPipe': '@angular/common',
75
+ 'SlicePipe': '@angular/common',
76
+ 'TitleCasePipe': '@angular/common',
77
+ 'UpperCasePipe': '@angular/common',
78
+ 'MatBadge': '@angular/material/badge',
79
+ 'MatButton': '@angular/material/button',
80
+ 'MatFabButton': '@angular/material/button',
81
+ 'MatIconButton': '@angular/material/button',
82
+ 'MatMiniFabButton': '@angular/material/button',
83
+ 'MatCheckbox': '@angular/material/checkbox',
84
+ 'MatChip': '@angular/material/chips',
85
+ 'MatChipSet': '@angular/material/chips',
86
+ 'MatDivider': '@angular/material/divider',
87
+ 'MatIcon': '@angular/material/icon',
88
+ 'MatMenu': '@angular/material/menu',
89
+ 'MatMenuItem': '@angular/material/menu',
90
+ 'MatMenuTrigger': '@angular/material/menu',
91
+ 'MatProgressBar': '@angular/material/progress-bar',
92
+ 'MatProgressSpinner': '@angular/material/progress-spinner',
93
+ 'MatSlideToggle': '@angular/material/slide-toggle',
94
+ 'MatTooltip': '@angular/material/tooltip'
95
+ };
96
+ /** The classes of a template by their selectors and pipes, in the order of angularClasses. */
97
+ const templateSelectors = [
98
+ [/\|\s*currency(?![\w-])/, 'CurrencyPipe'],
99
+ [/\|\s*date(?![\w-])/, 'DatePipe'],
100
+ [/\|\s*number(?![\w-])/, 'DecimalPipe'],
101
+ [/\|\s*lowercase(?![\w-])/, 'LowerCasePipe'],
102
+ [/\|\s*percent(?![\w-])/, 'PercentPipe'],
103
+ [/\|\s*slice(?![\w-])/, 'SlicePipe'],
104
+ [/\|\s*titlecase(?![\w-])/, 'TitleCasePipe'],
105
+ [/\|\s*uppercase(?![\w-])/, 'UpperCasePipe'],
106
+ [/\bmatBadge(?![\w-])/, 'MatBadge'],
107
+ [/\b(mat-button|mat-flat-button|mat-stroked-button|mat-raised-button|matButton)(?![\w-])/, 'MatButton'],
108
+ [/\b(mat-fab|matFab)(?![\w-])/, 'MatFabButton'],
109
+ [/\b(mat-icon-button|matIconButton)(?![\w-])/, 'MatIconButton'],
110
+ [/\b(mat-mini-fab|matMiniFab)(?![\w-])/, 'MatMiniFabButton'],
111
+ [/<mat-checkbox(?![\w-])/, 'MatCheckbox'],
112
+ [/<mat-chip(?![\w-])/, 'MatChip'],
113
+ [/<mat-chip-set(?![\w-])/, 'MatChipSet'],
114
+ [/<mat-divider(?![\w-])/, 'MatDivider'],
115
+ [/<mat-icon(?![\w-])/, 'MatIcon'],
116
+ [/<mat-menu(?![\w-])/, 'MatMenu'],
117
+ [/\bmat-menu-item(?![\w-])/, 'MatMenuItem'],
118
+ [/\bmatMenuTriggerFor(?![\w-])/, 'MatMenuTrigger'],
119
+ [/<mat-progress-bar(?![\w-])/, 'MatProgressBar'],
120
+ [/<(mat-progress-spinner|mat-spinner)(?![\w-])/, 'MatProgressSpinner'],
121
+ [/<mat-slide-toggle(?![\w-])/, 'MatSlideToggle'],
122
+ [/\bmatTooltip(?![\w-])/, 'MatTooltip']
123
+ ];
124
+ /**
125
+ * The classes of angularClasses that a template uses, found by their selectors and pipes: the imports of a generated
126
+ * component come from its template, so a model writes no imports and no NgModule gets into them.
127
+ */
128
+ export function templateClasses(template) {
129
+ return templateSelectors.filter(([selector]) => selector.test(template)).map(([, name]) => name);
130
+ }
@@ -0,0 +1,36 @@
1
+ import type { BroSpecFilter, BroSpecSkin, BroTableSpec } from '../spec/spec.js';
2
+ import type { BroSpecFillings } from './fillings.js';
3
+ /** A key of the object of the filters: a range gives two. */
4
+ interface BroFilterKey {
5
+ key: string;
6
+ type: 'string' | 'number' | 'boolean';
7
+ label: string;
8
+ }
9
+ /**
10
+ * The filters of a spec as code: the type of the object of the filters, the component of the Filter Drawer, and
11
+ * filtersTransform of the local mode.
12
+ */
13
+ export declare class BroFiltersCode {
14
+ private spec;
15
+ private fillings;
16
+ private readonly names;
17
+ private readonly fields;
18
+ constructor(spec: BroTableSpec, fillings?: BroSpecFillings);
19
+ get filters(): BroSpecFilter[];
20
+ /** The type of the object of the filters, after the type of the row. */
21
+ typeCode(): string;
22
+ /** filtersTransform of the local mode: every filter limits the rows, a cleared one is undefined and limits nothing. */
23
+ transformCode(): string;
24
+ /** The Angular component of the filters: the content of the Filter Drawer, with the fields of the skin. */
25
+ angularComponent(skin: BroSpecSkin): string;
26
+ /** The form of the filters in Vanilla JS: the content of the Filter Drawer with slot="filters". */
27
+ vanillaForm(skin: BroSpecSkin): string;
28
+ keys(): BroFilterKey[];
29
+ private valueType;
30
+ private options;
31
+ private angularField;
32
+ private vanillaField;
33
+ /** A field of the bs or the nv skin: Angular binds it with formControlName, Vanilla JS reads it by its name. */
34
+ private htmlField;
35
+ }
36
+ export {};
@@ -0,0 +1,254 @@
1
+ import { escapeHtml, escapeTemplate, imports, importsField, indentBlock, lineComment, objectKey, templateText } from './code.js';
2
+ import { BroNames } from './names.js';
3
+ /**
4
+ * The filters of a spec as code: the type of the object of the filters, the component of the Filter Drawer, and
5
+ * filtersTransform of the local mode.
6
+ */
7
+ export class BroFiltersCode {
8
+ spec;
9
+ fillings;
10
+ names;
11
+ fields;
12
+ constructor(spec, fillings = {}) {
13
+ this.spec = spec;
14
+ this.fillings = fillings;
15
+ this.names = BroNames.of(spec);
16
+ this.fields = new Map(spec.fields.map(field => [field.name, field]));
17
+ }
18
+ get filters() {
19
+ return this.spec.filters ?? [];
20
+ }
21
+ /** The type of the object of the filters, after the type of the row. */
22
+ typeCode() {
23
+ return [
24
+ `export type ${this.names.filtersType} = BroFilters<${this.spec.rowType}, {`,
25
+ ...this.keys().map(({ key, type }) => ` ${key}?: ${type};`),
26
+ '}>;'
27
+ ].join('\n');
28
+ }
29
+ /** filtersTransform of the local mode: every filter limits the rows, a cleared one is undefined and limits nothing. */
30
+ transformCode() {
31
+ const { rowType } = this.spec;
32
+ const rules = [];
33
+ const conditions = [];
34
+ for (const filter of this.filters) {
35
+ const value = `filters.${filter.name}`;
36
+ const row = filter.field ? `row.${filter.field}` : '';
37
+ if (filter.match === 'custom') {
38
+ const name = `matches${filter.name[0].toUpperCase()}${filter.name.slice(1)}`;
39
+ const filled = this.fillings.filterRules?.[filter.name]?.trim();
40
+ const body = filled || `// TODO: ${lineComment(filter.rule)}\nreturn true;`;
41
+ // A written rule without the value of its filter, as a checkbox, takes only the row
42
+ const withValue = !filled || /\bvalue\b/.test(filled);
43
+ rules.push(`// ${lineComment(filter.label)}: ${lineComment(filter.rule)}`, `const ${name} = (row: ${rowType}${withValue ? `, value: ${this.valueType(filter)}` : ''}): boolean => {`, indentBlock(body, ' '), '};', '');
44
+ conditions.push(`(${value} === undefined || ${name}(row${withValue ? `, ${value}` : ''}))`);
45
+ }
46
+ else if (filter.control === 'range') {
47
+ conditions.push(`matchesFilterCondition(${row}, {gte: filters.${filter.name}From, lte: filters.${filter.name}To})`);
48
+ }
49
+ else if (this.fields.get(filter.field)?.type === 'string[]') {
50
+ conditions.push(`(${value} === undefined || ${row}.includes(${value}))`);
51
+ }
52
+ else {
53
+ conditions.push(`matchesFilterCondition(${row}, {${filter.match ?? (filter.control === 'text' ? 'contains' : 'eq')}: ${value}})`);
54
+ }
55
+ }
56
+ const filterRows = `return rows.filter((row: ${rowType}) => {\n return ${conditions.join('\n && ')};\n});`;
57
+ if (!rules.length) {
58
+ return `(rows: ${rowType}[], filters: ${this.names.filtersType}) => rows.filter((row: ${rowType}) => {\n return ${conditions.join('\n && ')};\n})`;
59
+ }
60
+ return `(rows: ${rowType}[], filters: ${this.names.filtersType}) => {\n${indentBlock([...rules, filterRows].join('\n'), ' ')}\n}`;
61
+ }
62
+ /** The Angular component of the filters: the content of the Filter Drawer, with the fields of the skin. */
63
+ angularComponent(skin) {
64
+ const material = (skin === 'mat');
65
+ const angularModules = {
66
+ '@angular/core': ['ChangeDetectionStrategy', 'Component', 'EventEmitter', 'Output'],
67
+ '@angular/core/rxjs-interop': ['takeUntilDestroyed'],
68
+ '@angular/forms': ['FormControl', 'FormGroup', 'ReactiveFormsModule']
69
+ };
70
+ if (material) {
71
+ const controls = new Set(this.filters.map(filter => filter.control));
72
+ Object.assign(angularModules, {
73
+ '@angular/material/button': ['MatButton'],
74
+ '@angular/material/icon': ['MatIcon'],
75
+ '@angular/material/checkbox': controls.has('checkbox') ? ['MatCheckbox'] : [],
76
+ '@angular/material/form-field': (controls.has('text') || controls.has('select') || controls.has('range')) ? ['MatFormField', 'MatLabel'] : [],
77
+ '@angular/material/input': (controls.has('text') || controls.has('range')) ? ['MatInput'] : [],
78
+ '@angular/material/select': controls.has('select') ? ['MatOption', 'MatSelect'] : []
79
+ });
80
+ }
81
+ const classes = Object.fromEntries(Object.entries(angularModules)
82
+ .filter(([module]) => module === '@angular/forms' || module.startsWith('@angular/material/'))
83
+ .map(([module, names]) => [module, (module === '@angular/forms') ? ['ReactiveFormsModule'] : names]));
84
+ const markup = this.filters.map(filter => this.angularField(filter, skin)).join('\n\n');
85
+ const clear = {
86
+ mat: '<button matButton type="button" (click)="form.reset()">\n <mat-icon>filter_list_off</mat-icon>\n Clear\n </button>',
87
+ bs: '<button type="button" class="nav-link px-0" (click)="form.reset()">Clear</button>',
88
+ nv: '<a href="" class="bro-nav-link" (click)="$event.preventDefault(); form.reset()">Clear</a>'
89
+ }[skin];
90
+ const template = `<form [formGroup]="form">\n${indentBlock(markup, ' ')}\n\n ${clear}\n</form>`;
91
+ return [
92
+ imports({
93
+ ...angularModules,
94
+ rxjs: ['debounceTime'],
95
+ [`./${this.names.kebab}`]: [`type ${this.names.filtersType}`]
96
+ }),
97
+ '',
98
+ '/**',
99
+ ' * The filters of the Filter Drawer next to the table: the page gives them to the table with its input filter.',
100
+ ' * The output is a decorator (@Output), as in the cells of this table.',
101
+ ' */',
102
+ '@Component({',
103
+ ` selector: '${this.names.filtersSelector}',`,
104
+ ` template: \`\n${indentBlock(templateText(template), ' ')}\`,`,
105
+ ` styles: \`\n${indentBlock(styles[skin], ' ')}\`,`,
106
+ ' changeDetection: ChangeDetectionStrategy.OnPush,',
107
+ importsField(classes),
108
+ '})',
109
+ `export class ${this.names.filtersClass} {`,
110
+ ` @Output() changeFilter: EventEmitter<${this.names.filtersType}> = new EventEmitter<${this.names.filtersType}>();`,
111
+ '',
112
+ ' form = new FormGroup({',
113
+ this.keys().map(({ key, type }) => ` ${objectKey(key)}: new FormControl<${type} | null>(null)`).join(',\n'),
114
+ ' });',
115
+ '',
116
+ ' constructor() {',
117
+ ' // The table accumulates the changes of its filters, so a cleared field goes as an empty value',
118
+ ' this.form.valueChanges.pipe(debounceTime(400), takeUntilDestroyed()).subscribe(value => {',
119
+ ' this.changeFilter.emit({',
120
+ this.keys().map(({ key, type }) => ` ${objectKey(key)}: value.${key} ${type === 'number' ? '??' : '||'} undefined`).join(',\n'),
121
+ ' });',
122
+ ' });',
123
+ ' }',
124
+ '}',
125
+ ''
126
+ ].join('\n');
127
+ }
128
+ /** The form of the filters in Vanilla JS: the content of the Filter Drawer with slot="filters". */
129
+ vanillaForm(skin) {
130
+ // The native fields have no classes of a library: the form brings their rules
131
+ const style = (skin === 'nv') ? `<style>\n${indentBlock(styles.nv.replace(/\.app-native-field/g, `.${this.names.filtersSelector} .app-native-field`), ' ')}\n</style>\n\n` : '';
132
+ const markup = style + this.filters.map(filter => this.vanillaField(filter, skin)).join('\n\n');
133
+ const clear = (skin === 'bs') ? '<button type="button" class="nav-link px-0 app-filters-clear">Clear</button>' : '<a href="" class="bro-nav-link app-filters-clear">Clear</a>';
134
+ const read = this.keys().map(({ key, type }) => {
135
+ if (type === 'number') {
136
+ return ` ${objectKey(key)}: data.get('${key}') ? Number(data.get('${key}')) : undefined`;
137
+ }
138
+ return (type === 'boolean') ? ` ${objectKey(key)}: data.has('${key}') || undefined` : ` ${objectKey(key)}: (data.get('${key}') as string) || undefined`;
139
+ });
140
+ return [
141
+ imports({ [`type ./${this.names.kebab}`]: [this.names.filtersType] }),
142
+ '',
143
+ '/**',
144
+ ' * The form of the filters in the Filter Drawer next to the table, with slot="filters". It reports the filters',
145
+ ' * on every change; a cleared field goes as an empty value, as the table accumulates the changes of its filters.',
146
+ ' */',
147
+ `export function render${this.names.filtersClass}(onChange: (filters: ${this.names.filtersType}) => void): HTMLFormElement {`,
148
+ ' const form: HTMLFormElement = document.createElement(\'form\');',
149
+ '',
150
+ ` form.className = '${this.names.filtersSelector}';`,
151
+ ' form.setAttribute(\'slot\', \'filters\');',
152
+ ' form.setAttribute(\'autocomplete\', \'off\');',
153
+ ` form.innerHTML = \`\n${indentBlock(templateText(`${markup}\n\n${clear}`), ' ')}\`;`,
154
+ '',
155
+ ' const report = () => {',
156
+ ' const data = new FormData(form);',
157
+ '',
158
+ ' onChange({',
159
+ read.map(line => ' ' + line).join(',\n'),
160
+ ' });',
161
+ ' };',
162
+ '',
163
+ ' form.addEventListener(\'input\', report);',
164
+ ' form.querySelector(\'.app-filters-clear\')!.addEventListener(\'click\', (event: Event) => {',
165
+ ' event.preventDefault();',
166
+ '',
167
+ ' form.reset();',
168
+ ' report();',
169
+ ' });',
170
+ '',
171
+ ' return form;',
172
+ '}',
173
+ ''
174
+ ].join('\n');
175
+ }
176
+ keys() {
177
+ return this.filters.flatMap(filter => {
178
+ if (filter.control === 'range') {
179
+ return [
180
+ { key: `${filter.name}From`, type: 'number', label: `${filter.label} from` },
181
+ { key: `${filter.name}To`, type: 'number', label: `${filter.label} to` }
182
+ ];
183
+ }
184
+ return [{ key: filter.name, type: this.valueType(filter), label: filter.label }];
185
+ });
186
+ }
187
+ valueType(filter) {
188
+ return (filter.control === 'checkbox') ? 'boolean' : (filter.control === 'range') ? 'number' : 'string';
189
+ }
190
+ options(filter) {
191
+ return this.fields.get(filter.field ?? '')?.values ?? [];
192
+ }
193
+ angularField(filter, skin) {
194
+ const label = escapeTemplate(filter.label);
195
+ const keys = this.keys().filter(({ key }) => key === filter.name || key === `${filter.name}From` || key === `${filter.name}To`);
196
+ if (skin === 'mat') {
197
+ switch (filter.control) {
198
+ case 'text':
199
+ return matField(label, `<input matInput type="search" formControlName="${filter.name}">`);
200
+ case 'select':
201
+ return matField(label, [
202
+ `<mat-select formControlName="${filter.name}">`,
203
+ ' <mat-option [value]="null">Any</mat-option>',
204
+ ...this.options(filter).map(option => ` <mat-option value="${escapeTemplate(option)}">${escapeTemplate(option)}</mat-option>`),
205
+ '</mat-select>'
206
+ ].join('\n'));
207
+ case 'range':
208
+ return `<div class="app-filter-range">\n${indentBlock(keys.map(({ key, label }) => matField(escapeTemplate(label), `<input matInput type="number" formControlName="${key}">`)).join('\n'), ' ')}\n</div>`;
209
+ default:
210
+ return `<mat-checkbox class="app-filter" formControlName="${filter.name}">${label}</mat-checkbox>`;
211
+ }
212
+ }
213
+ return this.htmlField(filter, skin, true);
214
+ }
215
+ vanillaField(filter, skin) {
216
+ return this.htmlField(filter, skin, false);
217
+ }
218
+ /** A field of the bs or the nv skin: Angular binds it with formControlName, Vanilla JS reads it by its name. */
219
+ htmlField(filter, skin, angular) {
220
+ const escape = angular ? escapeTemplate : escapeHtml;
221
+ const bootstrap = (skin === 'bs');
222
+ const bind = (key) => angular ? `formControlName="${key}"` : `name="${key}"`;
223
+ const input = (key, type) => `<input type="${type}"${bootstrap ? ' class="form-control mt-1"' : ''} ${bind(key)}>`;
224
+ const field = (label, control) => {
225
+ return bootstrap ? `<label class="form-label w-100 mb-2">\n ${label}\n ${control}\n</label>` : `<label class="app-native-field">\n ${label}\n ${control}\n</label>`;
226
+ };
227
+ switch (filter.control) {
228
+ case 'text':
229
+ return field(escape(filter.label), input(filter.name, 'search'));
230
+ case 'select': {
231
+ const empty = angular ? '<option [ngValue]="null">Any</option>' : '<option value="">Any</option>';
232
+ const options = this.options(filter).map(option => `<option value="${escape(option)}">${escape(option)}</option>`);
233
+ return field(escape(filter.label), [`<select${bootstrap ? ' class="form-select mt-1"' : ''} ${bind(filter.name)}>`, ` ${empty}`, ...options.map(option => ` ${option}`), '</select>'].join('\n '));
234
+ }
235
+ case 'range':
236
+ return this.keys()
237
+ .filter(({ key }) => key === `${filter.name}From` || key === `${filter.name}To`)
238
+ .map(({ key, label }) => field(escape(label), input(key, 'number')))
239
+ .join('\n');
240
+ default:
241
+ return bootstrap ? `<div class="form-check mb-2">\n <label class="form-check-label">\n <input type="checkbox" class="form-check-input" ${bind(filter.name)}>\n ${escape(filter.label)}\n </label>\n</div>` : `<label class="app-native-field">\n <input type="checkbox" ${bind(filter.name)}>\n ${escape(filter.label)}\n</label>`;
242
+ }
243
+ }
244
+ }
245
+ /** A field of Angular Material as the filters of table-bro.com: without the place of the hint, the styles space the fields. */
246
+ function matField(label, control) {
247
+ return `<mat-form-field class="app-filter" subscriptSizing="dynamic">\n <mat-label>${label}</mat-label>\n${indentBlock(control, ' ')}\n</mat-form-field>`;
248
+ }
249
+ const styles = {
250
+ // The fields have no fill and stand 16 px apart, as the filters of table-bro.com
251
+ mat: ':host {\n --mat-form-field-filled-container-color: transparent;\n}\n\n.app-filter {\n display: block;\n width: 100%;\n margin-bottom: 16px;\n}\n\n.app-filter-range {\n display: flex;\n gap: 8px;\n}',
252
+ bs: ':host {\n display: block;\n}',
253
+ nv: '.app-native-field {\n display: block;\n margin-bottom: 12px;\n}\n\n.app-native-field input:not([type="checkbox"]),\n.app-native-field select {\n display: block;\n box-sizing: border-box;\n width: 100%;\n margin-top: 4px;\n}'
254
+ };
@@ -0,0 +1,34 @@
1
+ import type { BroMcpExtension } from '../extension.js';
2
+ import type { BroSpecSkin, BroTableSpec } from '../spec/spec.js';
3
+ import { type BroSpecLimits, type BroSpecReport } from '../spec/validate-spec.js';
4
+ import { type BroGeneratedFile } from './angular.js';
5
+ import { type BroSpecFillings } from './fillings.js';
6
+ import { type BroSetupStep, type BroTarget } from './setup.js';
7
+ export interface BroGenerateOptions {
8
+ target: BroTarget;
9
+ skin: BroSpecSkin;
10
+ /** The parts of the code that a model writes; without them the scaffolds have TODOs in their place. */
11
+ fillings?: BroSpecFillings;
12
+ /** The paid modules of the project: the generator writes their fields of the config and adds their setup. */
13
+ extensions?: BroMcpExtension[];
14
+ /** The suffix of the tags of the cells: tables generated into one page define their own Custom Elements. */
15
+ tagSuffix?: string;
16
+ /** Angular in the server mode: the page answers the queries of the table from the rows of the spec, as a server would. */
17
+ simulateServer?: boolean;
18
+ /** The sizes of a spec that the host takes. */
19
+ limits?: BroSpecLimits;
20
+ }
21
+ export interface BroGeneratedCode {
22
+ /** The check of the spec: with errors there are no files. */
23
+ report: BroSpecReport;
24
+ files: BroGeneratedFile[];
25
+ /** The setup of the project, once for all its tables. */
26
+ setup: BroSetupStep[];
27
+ /** What to finish by hand. */
28
+ todo: string[];
29
+ }
30
+ /**
31
+ * Writes the code of a table from its spec for a target and a skin: a pure function, the spec and the fillings in,
32
+ * the files out. It checks the spec first and writes nothing for a spec with errors.
33
+ */
34
+ export declare function generateCode(spec: BroTableSpec, options: BroGenerateOptions): BroGeneratedCode;