@nysds/codegen 0.0.0-stage → 1.22.1

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 (49) hide show
  1. package/README.md +95 -2
  2. package/cem-plugins/angular.mjs +551 -0
  3. package/cem-plugins/deps.mjs +154 -0
  4. package/cem-plugins/form-control.mjs +94 -0
  5. package/cem-plugins/index.mjs +12 -0
  6. package/cem-plugins/lib/core.d.mts +79 -0
  7. package/cem-plugins/lib/core.mjs +288 -0
  8. package/cem-plugins/react.mjs +143 -0
  9. package/cem-plugins/vue.mjs +228 -0
  10. package/dist/index.d.ts +17 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +14 -0
  13. package/dist/manifest/components.d.ts +6 -0
  14. package/dist/manifest/components.d.ts.map +1 -0
  15. package/dist/manifest/components.js +5 -0
  16. package/dist/manifest/filters.d.ts +6 -0
  17. package/dist/manifest/filters.d.ts.map +1 -0
  18. package/dist/manifest/filters.js +5 -0
  19. package/dist/manifest/form-control.d.ts +6 -0
  20. package/dist/manifest/form-control.d.ts.map +1 -0
  21. package/dist/manifest/form-control.js +5 -0
  22. package/dist/manifest/load.d.ts +11 -0
  23. package/dist/manifest/load.d.ts.map +1 -0
  24. package/dist/manifest/load.js +64 -0
  25. package/dist/manifest/naming.d.ts +7 -0
  26. package/dist/manifest/naming.d.ts.map +1 -0
  27. package/dist/manifest/naming.js +6 -0
  28. package/dist/manifest/packages.d.ts +6 -0
  29. package/dist/manifest/packages.d.ts.map +1 -0
  30. package/dist/manifest/packages.js +5 -0
  31. package/dist/manifest/types.d.ts +7 -0
  32. package/dist/manifest/types.d.ts.map +1 -0
  33. package/dist/manifest/types.js +1 -0
  34. package/dist/transform/index.d.ts +20 -0
  35. package/dist/transform/index.d.ts.map +1 -0
  36. package/dist/transform/index.js +11 -0
  37. package/dist/transform/parse.d.ts +19 -0
  38. package/dist/transform/parse.d.ts.map +1 -0
  39. package/dist/transform/parse.js +33 -0
  40. package/dist/transform/shared.d.ts +62 -0
  41. package/dist/transform/shared.d.ts.map +1 -0
  42. package/dist/transform/shared.js +96 -0
  43. package/dist/transform/to-angular.d.ts +7 -0
  44. package/dist/transform/to-angular.d.ts.map +1 -0
  45. package/dist/transform/to-angular.js +153 -0
  46. package/dist/transform/to-jsx.d.ts +7 -0
  47. package/dist/transform/to-jsx.d.ts.map +1 -0
  48. package/dist/transform/to-jsx.js +172 -0
  49. package/package.json +46 -4
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/transform/parse.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,QAAQ,CAAC;AAEpD,MAAM,MAAM,MAAM,GAAG,qBAAqB,CAAC,WAAW,CAAC,CAAC;AACxD,MAAM,MAAM,SAAS,GAAG,qBAAqB,CAAC,SAAS,CAAC,CAAC;AACzD,MAAM,MAAM,MAAM,GAAG,qBAAqB,CAAC,UAAU,CAAC,CAAC;AACvD,MAAM,MAAM,SAAS,GAAG,qBAAqB,CAAC,aAAa,CAAC,CAAC;AAE7D;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAExD;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,IAAI,SAAS,CAEzD;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,IAAI,MAAM,CAEnD;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,IAAI,SAAS,CAEzD;AAED,yDAAyD;AACzD,wBAAgB,YAAY,CAAC,OAAO,EAAE,SAAS,GAAG,MAAM,EAAE,CAMzD;AAED,sEAAsE;AACtE,wBAAgB,WAAW,CAAC,OAAO,EAAE,SAAS,GAAG,MAAM,CAKtD"}
@@ -0,0 +1,33 @@
1
+ import { parseFragment } from "parse5";
2
+ /**
3
+ * Parses an HTML fragment (a docs `preview` block or an `@example` block)
4
+ * into parse5 nodes. parse5 lowercases element and attribute names, matching
5
+ * what a browser does to the same markup.
6
+ */
7
+ export function parseHtmlFragment(html) {
8
+ return parseFragment(html).childNodes;
9
+ }
10
+ export function isElement(node) {
11
+ return "tagName" in node;
12
+ }
13
+ export function isText(node) {
14
+ return node.nodeName === "#text";
15
+ }
16
+ export function isComment(node) {
17
+ return node.nodeName === "#comment";
18
+ }
19
+ /** Child nodes, reading through `<template>` content. */
20
+ export function childNodesOf(element) {
21
+ const template = element;
22
+ if (template.content) {
23
+ return template.content.childNodes;
24
+ }
25
+ return element.childNodes;
26
+ }
27
+ /** Concatenated text content of an element's direct text children. */
28
+ export function textContent(element) {
29
+ return childNodesOf(element)
30
+ .filter(isText)
31
+ .map((node) => node.value)
32
+ .join("");
33
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Rules shared by both emitters: component lookup, attribute
3
+ * classification, and the script/style/inline-handler policy.
4
+ */
5
+ import type { ComponentMeta, Manifest, PropMeta } from "../manifest/types.js";
6
+ import { type P5Element } from "./parse.js";
7
+ export type FormsMode = "none" | "template" | "reactive";
8
+ export interface TransformResult {
9
+ /** The transformed snippet. Imports are listed separately. */
10
+ code: string;
11
+ language: "jsx" | "html";
12
+ warnings: string[];
13
+ /**
14
+ * True when the example depends on something the target framework snippet
15
+ * cannot express (imperative script against a component, inline handlers).
16
+ * The code is still returned as a best effort.
17
+ */
18
+ unsupported: boolean;
19
+ /**
20
+ * Names the docs partial needs to import: component class names for
21
+ * React, Angular component class names for Angular.
22
+ */
23
+ imports: string[];
24
+ }
25
+ export interface TransformContext {
26
+ byTag: Map<string, ComponentMeta>;
27
+ propLookups: Map<string, Map<string, PropMeta>>;
28
+ warnings: string[];
29
+ imports: Set<string>;
30
+ unsupported: boolean;
31
+ formsMode: FormsMode;
32
+ }
33
+ export declare function buildContext(manifest: Manifest, formsMode: FormsMode): TransformContext;
34
+ /**
35
+ * Props indexed by lowercased attribute name and lowercased property name.
36
+ * parse5 lowercases attribute names, so a camelCase manifest attribute
37
+ * (`errorMessage`) arrives as `errormessage`.
38
+ */
39
+ export declare function propLookup(context: TransformContext, component: ComponentMeta): Map<string, PropMeta>;
40
+ /** True for a component tag from this design system. */
41
+ export declare function isNysTag(tagName: string): boolean;
42
+ /** True for `onclick`-style inline handler attributes. */
43
+ export declare function isInlineHandler(attrName: string): boolean;
44
+ /** Global attributes that pass through without a warning. */
45
+ export declare function isSilentGlobalAttr(attrName: string): boolean;
46
+ /**
47
+ * Handles `<script>`, `<style>`, and inline handlers for both emitters.
48
+ * Returns true when the element was consumed (dropped) by the policy.
49
+ */
50
+ export declare function dropScriptOrStyle(context: TransformContext, element: P5Element): boolean;
51
+ export declare function warnInlineHandler(context: TransformContext, attrName: string, tagName: string): void;
52
+ export declare function warnUnknownAttr(context: TransformContext, attrName: string, tagName: string): void;
53
+ /** True when an attribute value is a numeric literal. */
54
+ export declare function isNumericLiteral(value: string): boolean;
55
+ /**
56
+ * The name a form model field gets in generated form bindings, taken from
57
+ * the example's own `name` (then `id`) attribute, then the component
58
+ * subpath.
59
+ */
60
+ export declare function modelFieldName(element: P5Element, component: ComponentMeta): string;
61
+ export declare function elementChildren(element: P5Element): P5Element[];
62
+ //# sourceMappingURL=shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/transform/shared.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAG9E,OAAO,EAA0B,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AAEpE,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,UAAU,GAAG,UAAU,CAAC;AAEzD,MAAM,WAAW,eAAe;IAC9B,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,KAAK,GAAG,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB;;;;OAIG;IACH,WAAW,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAClC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;IAChD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACrB,WAAW,EAAE,OAAO,CAAC;IACrB,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,SAAS,GACnB,gBAAgB,CAalB;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,gBAAgB,EACzB,SAAS,EAAE,aAAa,GACvB,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAavB;AAED,wDAAwD;AACxD,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED,0DAA0D;AAC1D,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED,6DAA6D;AAC7D,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAQ5D;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,gBAAgB,EACzB,OAAO,EAAE,SAAS,GACjB,OAAO,CAcT;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,gBAAgB,EACzB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACd,IAAI,CAKN;AAED,wBAAgB,eAAe,CAC7B,OAAO,EAAE,gBAAgB,EACzB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACd,IAAI,CAIN;AAED,yDAAyD;AACzD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,SAAS,EAClB,SAAS,EAAE,aAAa,GACvB,MAAM,CAMR;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,EAAE,CAE/D"}
@@ -0,0 +1,96 @@
1
+ import { listComponents } from "../manifest/components.js";
2
+ import { NYS_PREFIX } from "../manifest/naming.js";
3
+ import { isElement, textContent } from "./parse.js";
4
+ export function buildContext(manifest, formsMode) {
5
+ const byTag = new Map();
6
+ for (const component of listComponents(manifest)) {
7
+ byTag.set(component.tag, component);
8
+ }
9
+ return {
10
+ byTag,
11
+ propLookups: new Map(),
12
+ warnings: [],
13
+ imports: new Set(),
14
+ unsupported: false,
15
+ formsMode,
16
+ };
17
+ }
18
+ /**
19
+ * Props indexed by lowercased attribute name and lowercased property name.
20
+ * parse5 lowercases attribute names, so a camelCase manifest attribute
21
+ * (`errorMessage`) arrives as `errormessage`.
22
+ */
23
+ export function propLookup(context, component) {
24
+ let lookup = context.propLookups.get(component.tag);
25
+ if (!lookup) {
26
+ lookup = new Map();
27
+ for (const prop of component.props) {
28
+ if (prop.attribute)
29
+ lookup.set(prop.attribute.toLowerCase(), prop);
30
+ if (!lookup.has(prop.name.toLowerCase())) {
31
+ lookup.set(prop.name.toLowerCase(), prop);
32
+ }
33
+ }
34
+ context.propLookups.set(component.tag, lookup);
35
+ }
36
+ return lookup;
37
+ }
38
+ /** True for a component tag from this design system. */
39
+ export function isNysTag(tagName) {
40
+ return tagName.startsWith(NYS_PREFIX);
41
+ }
42
+ /** True for `onclick`-style inline handler attributes. */
43
+ export function isInlineHandler(attrName) {
44
+ return /^on[a-z]/.test(attrName);
45
+ }
46
+ /** Global attributes that pass through without a warning. */
47
+ export function isSilentGlobalAttr(attrName) {
48
+ return (attrName === "id" ||
49
+ attrName === "name" ||
50
+ attrName === "part" ||
51
+ attrName.startsWith("data-") ||
52
+ attrName.startsWith("aria-"));
53
+ }
54
+ /**
55
+ * Handles `<script>`, `<style>`, and inline handlers for both emitters.
56
+ * Returns true when the element was consumed (dropped) by the policy.
57
+ */
58
+ export function dropScriptOrStyle(context, element) {
59
+ if (element.tagName !== "script" && element.tagName !== "style") {
60
+ return false;
61
+ }
62
+ context.warnings.push(`Dropped <${element.tagName}> block: examples must be declarative markup.`);
63
+ if (element.tagName === "script" &&
64
+ textContent(element).includes(NYS_PREFIX)) {
65
+ context.unsupported = true;
66
+ }
67
+ return true;
68
+ }
69
+ export function warnInlineHandler(context, attrName, tagName) {
70
+ context.warnings.push(`Inline handler "${attrName}" on <${tagName}> cannot be transformed.`);
71
+ context.unsupported = true;
72
+ }
73
+ export function warnUnknownAttr(context, attrName, tagName) {
74
+ context.warnings.push(`Unknown attribute "${attrName}" on <${tagName}> passed through unchanged.`);
75
+ }
76
+ /** True when an attribute value is a numeric literal. */
77
+ export function isNumericLiteral(value) {
78
+ return /^-?\d+(\.\d+)?$/.test(value);
79
+ }
80
+ /**
81
+ * The name a form model field gets in generated form bindings, taken from
82
+ * the example's own `name` (then `id`) attribute, then the component
83
+ * subpath.
84
+ */
85
+ export function modelFieldName(element, component) {
86
+ const byName = element.attrs.find((attr) => attr.name === "name")?.value;
87
+ if (byName)
88
+ return byName;
89
+ const byId = element.attrs.find((attr) => attr.name === "id")?.value;
90
+ if (byId)
91
+ return byId;
92
+ return component.subpath;
93
+ }
94
+ export function elementChildren(element) {
95
+ return element.childNodes.filter(isElement);
96
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * HTML example to Angular template snippet.
3
+ */
4
+ import type { Manifest } from "../manifest/types.js";
5
+ import { type FormsMode, type TransformResult } from "./shared.js";
6
+ export declare function toAngular(html: string, manifest: Manifest, formsMode?: FormsMode): TransformResult;
7
+ //# sourceMappingURL=to-angular.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"to-angular.d.ts","sourceRoot":"","sources":["../../src/transform/to-angular.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,QAAQ,EAA2B,MAAM,sBAAsB,CAAC;AAW9E,OAAO,EAWL,KAAK,SAAS,EAEd,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AAqBrB,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,QAAQ,EAClB,SAAS,GAAE,SAAkB,GAC5B,eAAe,CAUjB"}
@@ -0,0 +1,153 @@
1
+ import { classToAngularClass } from "../manifest/naming.js";
2
+ import { parseHtmlFragment, isElement, isText, isComment, childNodesOf, } from "./parse.js";
3
+ import { buildContext, propLookup, isNysTag, isInlineHandler, isSilentGlobalAttr, dropScriptOrStyle, warnInlineHandler, warnUnknownAttr, isNumericLiteral, modelFieldName, } from "./shared.js";
4
+ const INLINE_TEXT_MAX = 60;
5
+ /** Native elements with no closing tag. */
6
+ const VOID_ELEMENTS = new Set([
7
+ "area",
8
+ "base",
9
+ "br",
10
+ "col",
11
+ "embed",
12
+ "hr",
13
+ "img",
14
+ "input",
15
+ "link",
16
+ "meta",
17
+ "source",
18
+ "track",
19
+ "wbr",
20
+ ]);
21
+ export function toAngular(html, manifest, formsMode = "none") {
22
+ const context = buildContext(manifest, formsMode);
23
+ const lines = renderNodes(context, parseHtmlFragment(html), 0);
24
+ return {
25
+ code: lines.length > 0 ? lines.join("\n") + "\n" : "",
26
+ language: "html",
27
+ warnings: context.warnings,
28
+ unsupported: context.unsupported,
29
+ imports: [...context.imports].sort(),
30
+ };
31
+ }
32
+ function renderNodes(context, nodes, depth) {
33
+ const indent = " ".repeat(depth);
34
+ const lines = [];
35
+ for (const node of nodes) {
36
+ if (isText(node)) {
37
+ const text = node.value.replace(/\s+/g, " ").trim();
38
+ if (text)
39
+ lines.push(indent + text);
40
+ continue;
41
+ }
42
+ if (isComment(node)) {
43
+ lines.push(`${indent}<!-- ${node.data.trim()} -->`);
44
+ continue;
45
+ }
46
+ if (isElement(node)) {
47
+ lines.push(...renderElement(context, node, depth));
48
+ }
49
+ }
50
+ return lines;
51
+ }
52
+ function renderElement(context, element, depth) {
53
+ if (dropScriptOrStyle(context, element))
54
+ return [];
55
+ const indent = " ".repeat(depth);
56
+ const tagName = element.tagName;
57
+ let component;
58
+ if (isNysTag(tagName)) {
59
+ component = context.byTag.get(tagName);
60
+ if (!component) {
61
+ context.warnings.push(`Unknown component <${tagName}> is not in the manifest; passed through unchanged.`);
62
+ }
63
+ }
64
+ if (component) {
65
+ context.imports.add(classToAngularClass(component.className));
66
+ }
67
+ const formControl = component && context.formsMode !== "none"
68
+ ? component.formControl
69
+ : undefined;
70
+ const attrParts = [];
71
+ for (const attr of element.attrs) {
72
+ if (isInlineHandler(attr.name)) {
73
+ warnInlineHandler(context, attr.name, tagName);
74
+ continue;
75
+ }
76
+ if (formControl &&
77
+ ((formControl.kind === "value" && attr.name === "value") ||
78
+ (formControl.kind === "checked" && attr.name === "checked"))) {
79
+ // The form binding owns the value.
80
+ continue;
81
+ }
82
+ if (attr.name === "class" ||
83
+ attr.name === "slot" ||
84
+ attr.name === "style") {
85
+ attrParts.push(htmlAttr(attr.name, attr.value));
86
+ continue;
87
+ }
88
+ if (component) {
89
+ const prop = propLookup(context, component).get(attr.name);
90
+ if (prop) {
91
+ attrParts.push(angularPropAttr(prop, attr.value));
92
+ continue;
93
+ }
94
+ if (isSilentGlobalAttr(attr.name)) {
95
+ attrParts.push(htmlAttr(attr.name, attr.value));
96
+ continue;
97
+ }
98
+ warnUnknownAttr(context, attr.name, tagName);
99
+ attrParts.push(htmlAttr(attr.name, attr.value));
100
+ continue;
101
+ }
102
+ attrParts.push(htmlAttr(attr.name, attr.value));
103
+ }
104
+ if (formControl && component) {
105
+ const field = modelFieldName(element, component);
106
+ if (context.formsMode === "template") {
107
+ attrParts.push(`[(ngModel)]="model.${field}"`);
108
+ }
109
+ else if (context.formsMode === "reactive") {
110
+ attrParts.push(`formControlName="${field}"`);
111
+ }
112
+ }
113
+ const attrsText = attrParts.length > 0 ? ` ${attrParts.join(" ")}` : "";
114
+ if (VOID_ELEMENTS.has(tagName)) {
115
+ return [`${indent}<${tagName}${attrsText}>`];
116
+ }
117
+ const children = childNodesOf(element);
118
+ const childLines = renderNodes(context, children, depth + 1);
119
+ if (childLines.length === 0) {
120
+ return [`${indent}<${tagName}${attrsText}></${tagName}>`];
121
+ }
122
+ const onlyText = children.every((node) => !isElement(node) && !isComment(node));
123
+ if (onlyText && childLines.length === 1) {
124
+ const text = childLines[0].trim();
125
+ if (text.length <= INLINE_TEXT_MAX) {
126
+ return [`${indent}<${tagName}${attrsText}>${text}</${tagName}>`];
127
+ }
128
+ }
129
+ return [
130
+ `${indent}<${tagName}${attrsText}>`,
131
+ ...childLines,
132
+ `${indent}</${tagName}>`,
133
+ ];
134
+ }
135
+ function htmlAttr(name, value) {
136
+ if (value === "")
137
+ return name;
138
+ return value.includes('"') ? `${name}='${value}'` : `${name}="${value}"`;
139
+ }
140
+ function angularPropAttr(prop, value) {
141
+ if (prop.isBoolean) {
142
+ return value === "false" ? `[${prop.name}]="false"` : prop.name;
143
+ }
144
+ if (prop.isNumber && isNumericLiteral(value)) {
145
+ return `[${prop.name}]="${value}"`;
146
+ }
147
+ if (!prop.isPrimitive && value !== "") {
148
+ return value.includes('"')
149
+ ? `[${prop.name}]='${value}'`
150
+ : `[${prop.name}]="${value}"`;
151
+ }
152
+ return htmlAttr(prop.name, value);
153
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * HTML example to JSX snippet.
3
+ */
4
+ import type { Manifest } from "../manifest/types.js";
5
+ import { type FormsMode, type TransformResult } from "./shared.js";
6
+ export declare function toJsx(html: string, manifest: Manifest, formsMode?: FormsMode): TransformResult;
7
+ //# sourceMappingURL=to-jsx.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"to-jsx.d.ts","sourceRoot":"","sources":["../../src/transform/to-jsx.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,QAAQ,EAA2B,MAAM,sBAAsB,CAAC;AAW9E,OAAO,EAUL,KAAK,SAAS,EAEd,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AAIrB,wBAAgB,KAAK,CACnB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,QAAQ,EAClB,SAAS,GAAE,SAAkB,GAC5B,eAAe,CAUjB"}
@@ -0,0 +1,172 @@
1
+ import { eventToReactProp } from "../manifest/naming.js";
2
+ import { parseHtmlFragment, isElement, isText, isComment, childNodesOf, } from "./parse.js";
3
+ import { buildContext, propLookup, isNysTag, isInlineHandler, isSilentGlobalAttr, dropScriptOrStyle, warnInlineHandler, warnUnknownAttr, isNumericLiteral, } from "./shared.js";
4
+ const INLINE_TEXT_MAX = 60;
5
+ export function toJsx(html, manifest, formsMode = "none") {
6
+ const context = buildContext(manifest, formsMode);
7
+ const lines = renderNodes(context, parseHtmlFragment(html), 0);
8
+ return {
9
+ code: lines.length > 0 ? lines.join("\n") + "\n" : "",
10
+ language: "jsx",
11
+ warnings: context.warnings,
12
+ unsupported: context.unsupported,
13
+ imports: [...context.imports].sort(),
14
+ };
15
+ }
16
+ function renderNodes(context, nodes, depth) {
17
+ const indent = " ".repeat(depth);
18
+ const lines = [];
19
+ for (const node of nodes) {
20
+ if (isText(node)) {
21
+ const text = node.value.replace(/\s+/g, " ").trim();
22
+ if (text)
23
+ lines.push(indent + escapeJsxText(text));
24
+ continue;
25
+ }
26
+ if (isComment(node)) {
27
+ lines.push(`${indent}{/* ${node.data.trim()} */}`);
28
+ continue;
29
+ }
30
+ if (isElement(node)) {
31
+ lines.push(...renderElement(context, node, depth));
32
+ }
33
+ }
34
+ return lines;
35
+ }
36
+ function renderElement(context, element, depth) {
37
+ if (dropScriptOrStyle(context, element))
38
+ return [];
39
+ const indent = " ".repeat(depth);
40
+ const tagName = element.tagName;
41
+ let component;
42
+ if (isNysTag(tagName)) {
43
+ component = context.byTag.get(tagName);
44
+ if (!component) {
45
+ context.warnings.push(`Unknown component <${tagName}> is not in the manifest; passed through unchanged.`);
46
+ }
47
+ }
48
+ const jsxName = component ? component.className : tagName;
49
+ if (component)
50
+ context.imports.add(component.className);
51
+ const formControl = component && context.formsMode !== "none"
52
+ ? component.formControl
53
+ : undefined;
54
+ const controlledAttr = formControl?.kind === "value"
55
+ ? "value"
56
+ : formControl?.kind === "checked"
57
+ ? "checked"
58
+ : undefined;
59
+ const isControlled = controlledAttr !== undefined &&
60
+ element.attrs.some((attr) => attr.name === controlledAttr);
61
+ const attrParts = [];
62
+ for (const attr of element.attrs) {
63
+ if (isInlineHandler(attr.name)) {
64
+ warnInlineHandler(context, attr.name, tagName);
65
+ continue;
66
+ }
67
+ if (isControlled && attr.name === controlledAttr)
68
+ continue;
69
+ if (attr.name === "class") {
70
+ attrParts.push(jsxStringAttr("className", attr.value));
71
+ continue;
72
+ }
73
+ if (attr.name === "style") {
74
+ const style = styleToJsx(context, attr.value, tagName);
75
+ if (style)
76
+ attrParts.push(style);
77
+ continue;
78
+ }
79
+ if (attr.name === "slot") {
80
+ attrParts.push(jsxStringAttr("slot", attr.value));
81
+ continue;
82
+ }
83
+ if (component) {
84
+ const prop = propLookup(context, component).get(attr.name);
85
+ if (prop) {
86
+ attrParts.push(jsxPropAttr(prop, attr.value));
87
+ continue;
88
+ }
89
+ if (isSilentGlobalAttr(attr.name)) {
90
+ attrParts.push(jsxStringAttr(attr.name, attr.value));
91
+ continue;
92
+ }
93
+ warnUnknownAttr(context, attr.name, tagName);
94
+ attrParts.push(jsxStringAttr(attr.name, attr.value));
95
+ continue;
96
+ }
97
+ // Native or unknown element.
98
+ if (attr.name === "for") {
99
+ attrParts.push(jsxStringAttr("htmlFor", attr.value));
100
+ continue;
101
+ }
102
+ attrParts.push(jsxStringAttr(attr.name, attr.value));
103
+ }
104
+ if (isControlled && formControl && controlledAttr) {
105
+ const stateName = controlledAttr;
106
+ const setter = `set${stateName[0].toUpperCase()}${stateName.slice(1)}`;
107
+ attrParts.push(`${stateName}={${stateName}}`);
108
+ attrParts.push(`${eventToReactProp(formControl.changeEvent)}={(e) => ${setter}(e.detail.${stateName})}`);
109
+ }
110
+ const attrsText = attrParts.length > 0 ? ` ${attrParts.join(" ")}` : "";
111
+ const children = childNodesOf(element);
112
+ const childLines = renderNodes(context, children, depth + 1);
113
+ if (childLines.length === 0) {
114
+ return [`${indent}<${jsxName}${attrsText} />`];
115
+ }
116
+ const onlyText = children.every((node) => !isElement(node) && !isComment(node));
117
+ if (onlyText && childLines.length === 1) {
118
+ const text = childLines[0].trim();
119
+ if (text.length <= INLINE_TEXT_MAX) {
120
+ return [`${indent}<${jsxName}${attrsText}>${text}</${jsxName}>`];
121
+ }
122
+ }
123
+ return [
124
+ `${indent}<${jsxName}${attrsText}>`,
125
+ ...childLines,
126
+ `${indent}</${jsxName}>`,
127
+ ];
128
+ }
129
+ function escapeJsxText(text) {
130
+ return /[{}]/.test(text) ? `{${JSON.stringify(text)}}` : text;
131
+ }
132
+ function jsxStringAttr(name, value) {
133
+ return value.includes('"')
134
+ ? `${name}={${JSON.stringify(value)}}`
135
+ : `${name}="${value}"`;
136
+ }
137
+ function jsxPropAttr(prop, value) {
138
+ if (prop.isBoolean) {
139
+ return value === "false" ? `${prop.name}={false}` : prop.name;
140
+ }
141
+ if (prop.isNumber && isNumericLiteral(value)) {
142
+ return `${prop.name}={${value}}`;
143
+ }
144
+ if (!prop.isPrimitive && (value.startsWith("{") || value.startsWith("["))) {
145
+ return `${prop.name}={${value}}`;
146
+ }
147
+ return jsxStringAttr(prop.name, value);
148
+ }
149
+ /** Converts an inline style string to a JSX style object. */
150
+ function styleToJsx(context, value, tagName) {
151
+ const declarations = value
152
+ .split(";")
153
+ .map((declaration) => declaration.trim())
154
+ .filter(Boolean);
155
+ const pairs = [];
156
+ for (const declaration of declarations) {
157
+ const colon = declaration.indexOf(":");
158
+ if (colon === -1) {
159
+ context.warnings.push(`Dropped unparseable style attribute on <${tagName}>.`);
160
+ return undefined;
161
+ }
162
+ const property = declaration
163
+ .slice(0, colon)
164
+ .trim()
165
+ .replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
166
+ const propertyValue = declaration.slice(colon + 1).trim();
167
+ pairs.push(`${property}: ${JSON.stringify(propertyValue)}`);
168
+ }
169
+ if (pairs.length === 0)
170
+ return undefined;
171
+ return `style={{ ${pairs.join(", ")} }}`;
172
+ }
package/package.json CHANGED
@@ -1,6 +1,48 @@
1
1
  {
2
2
  "name": "@nysds/codegen",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.22.1",
4
+ "description": "Manifest readers, naming rules, CEM plugins, and the HTML snippet transformer for NYSDS framework support.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./cem-plugins": "./cem-plugins/index.mjs",
15
+ "./cem-plugins/*": "./cem-plugins/*",
16
+ "./package.json": "./package.json"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "cem-plugins",
21
+ "README.md"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsc -b",
25
+ "test": "tsc -b && node --test"
26
+ },
27
+ "dependencies": {
28
+ "parse5": "^7.3.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^22.0.0",
32
+ "typescript": "^5.9.3"
33
+ },
34
+ "keywords": [
35
+ "new-york-state",
36
+ "design-system",
37
+ "codegen",
38
+ "custom-elements-manifest"
39
+ ],
40
+ "author": "New York State Design System Team",
41
+ "license": "MIT",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/ITS-HCD/nysds.git",
45
+ "directory": "packages/codegen"
46
+ },
47
+ "sideEffects": false
48
+ }