@maggioli-design-system/magma-codemods 2.0.0-beta.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 (44) hide show
  1. package/README.md +193 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +77 -0
  4. package/dist/generate/diff-docs.d.ts +36 -0
  5. package/dist/generate/diff-docs.js +180 -0
  6. package/dist/index.d.ts +39 -0
  7. package/dist/index.js +166 -0
  8. package/dist/manifest/manifest.d.ts +3 -0
  9. package/dist/manifest/manifest.generated.d.ts +2 -0
  10. package/dist/manifest/manifest.generated.js +1901 -0
  11. package/dist/manifest/manifest.js +546 -0
  12. package/dist/manifest/registry.d.ts +32 -0
  13. package/dist/manifest/registry.js +102 -0
  14. package/dist/manifest/schema.d.ts +245 -0
  15. package/dist/manifest/schema.js +9 -0
  16. package/dist/report/diff.d.ts +2 -0
  17. package/dist/report/diff.js +95 -0
  18. package/dist/report/reporter.d.ts +27 -0
  19. package/dist/report/reporter.js +118 -0
  20. package/dist/report/types.d.ts +51 -0
  21. package/dist/report/types.js +2 -0
  22. package/dist/surfaces/angular.d.ts +3 -0
  23. package/dist/surfaces/angular.js +489 -0
  24. package/dist/surfaces/css.d.ts +5 -0
  25. package/dist/surfaces/css.js +287 -0
  26. package/dist/surfaces/html.d.ts +3 -0
  27. package/dist/surfaces/html.js +402 -0
  28. package/dist/surfaces/inline-templates.d.ts +3 -0
  29. package/dist/surfaces/inline-templates.js +98 -0
  30. package/dist/surfaces/react.d.ts +3 -0
  31. package/dist/surfaces/react.js +496 -0
  32. package/dist/surfaces/shared/attribute-ops.d.ts +56 -0
  33. package/dist/surfaces/shared/attribute-ops.js +62 -0
  34. package/dist/surfaces/shared/class-ops.d.ts +55 -0
  35. package/dist/surfaces/shared/class-ops.js +91 -0
  36. package/dist/surfaces/shared/edits.d.ts +19 -0
  37. package/dist/surfaces/shared/edits.js +30 -0
  38. package/dist/surfaces/shared/negate.d.ts +13 -0
  39. package/dist/surfaces/shared/negate.js +51 -0
  40. package/dist/surfaces/shared/transform.d.ts +17 -0
  41. package/dist/surfaces/shared/transform.js +8 -0
  42. package/dist/surfaces/shared/value-model.d.ts +49 -0
  43. package/dist/surfaces/shared/value-model.js +18 -0
  44. package/package.json +61 -0
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Surface-agnostic transformation core. These functions take a parsed value
3
+ * (see {@link AttrValue}) plus the primitive parameters of a manifest rule and
4
+ * return an *outcome* describing what the surface should render — they never
5
+ * touch a concrete AST, so the logic is written and tested once.
6
+ */
7
+ import { resolveBoolean } from './value-model.js';
8
+ import { negateExprText } from './negate.js';
9
+ /**
10
+ * Invert a boolean prop's value (category B). The new prop's value is the
11
+ * logical negation of the old one; `newDefault` decides whether that value can
12
+ * be omitted. `oldDefault` is intentionally unused — for a boolean prop the
13
+ * shorthand always means `true` regardless of the declared default.
14
+ */
15
+ export const invertBoolean = (current, newDefault) => {
16
+ const resolved = resolveBoolean(current);
17
+ if (resolved.kind === 'literal') {
18
+ return outcomeForLiteral(!resolved.value, newDefault);
19
+ }
20
+ const negated = negateExprText(resolved.expr);
21
+ // The negation may collapse to a literal (e.g. the expr was `true`/`false`).
22
+ if (negated === 'true')
23
+ return outcomeForLiteral(true, newDefault);
24
+ if (negated === 'false')
25
+ return outcomeForLiteral(false, newDefault);
26
+ return { action: 'expr', expr: negated };
27
+ };
28
+ const outcomeForLiteral = (value, newDefault) => {
29
+ if (value === newDefault)
30
+ return { action: 'omit' };
31
+ return value ? { action: 'shorthandTrue' } : { action: 'explicitFalse' };
32
+ };
33
+ /**
34
+ * Remap an enum literal, validating the target against the component's v2 enum
35
+ * set when available. Dynamic values, map targets that are `null` (no 1:1
36
+ * replacement) and literals absent from the v2 set are flagged for the report
37
+ * instead of being rewritten blindly.
38
+ */
39
+ export const remapEnum = (current, map, v2set) => {
40
+ if (current.kind === 'dynamic') {
41
+ return { action: 'flag', reason: 'dynamic value, cannot remap enum' };
42
+ }
43
+ if (current.kind !== 'stringLiteral') {
44
+ return { action: 'flag', reason: `non-string value for enum prop (${current.kind})` };
45
+ }
46
+ const value = current.value;
47
+ if (Object.prototype.hasOwnProperty.call(map, value)) {
48
+ const target = map[value];
49
+ if (target === null || target === undefined) {
50
+ return { action: 'flag', reason: `"${value}" has no v2 equivalent, migrate manually` };
51
+ }
52
+ if (v2set && !v2set.includes(target)) {
53
+ return { action: 'flag', reason: `mapped target "${target}" is not in the v2 set` };
54
+ }
55
+ return { action: 'rename', value: target };
56
+ }
57
+ // Not in the map: keep it if it is already a valid v2 value, otherwise flag.
58
+ if (v2set && !v2set.includes(value)) {
59
+ return { action: 'flag', reason: `"${value}" is not a valid v2 value for this prop` };
60
+ }
61
+ return { action: 'keep' };
62
+ };
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Utility-class rewriting shared by the HTML / React / Angular / CSS surfaces
3
+ * (category J). The manifest lists BARE utility names; markup carries them
4
+ * under Tailwind variant prefixes (`hover:`, `md:`, arbitrary variants like
5
+ * `[&>li]:`) and important markers (leading `!` in v1's Tailwind 3, trailing
6
+ * `!` in Tailwind 4), so tokens are matched on the bare segment and only that
7
+ * segment is rewritten. The lookup is a single pass per token: the rename map
8
+ * contains chains (`rounded-xl → rounded-md` while `rounded-md → rounded-2xs`),
9
+ * and one lookup per token guarantees a rename never cascades.
10
+ */
11
+ import { type ClassRenameRule, type ClassReportRule, type Manifest } from '../../manifest/schema.js';
12
+ export interface ClassRenameEntry {
13
+ rule: ClassRenameRule;
14
+ id: string;
15
+ }
16
+ export interface ClassReportEntry {
17
+ rule: ClassReportRule;
18
+ id: string;
19
+ }
20
+ export interface ClassRules {
21
+ renames: Map<string, ClassRenameEntry>;
22
+ reports: Map<string, ClassReportEntry>;
23
+ /** Cheap probe: does this chunk of source mention any migrated class at all? */
24
+ candidateRe: RegExp | null;
25
+ }
26
+ export declare const classRulesOf: (manifest: Manifest) => ClassRules;
27
+ export declare const hasClassRules: (rules: ClassRules) => boolean;
28
+ interface SplitToken {
29
+ /** Variant prefix including its trailing colon(s): `hover:`, `md:[&>li]:`. */
30
+ prefix: string;
31
+ /** Leading important marker (Tailwind 3). */
32
+ bang: string;
33
+ /** The bare utility name the manifest rules are keyed on. */
34
+ base: string;
35
+ /** Trailing important marker (Tailwind 4). */
36
+ trailingBang: string;
37
+ }
38
+ /**
39
+ * Split one class token into variant prefix, important markers and the bare
40
+ * utility. The prefix ends at the last `:` outside square brackets, so
41
+ * arbitrary variants (`[&>li]:`, `[@media(hover)]:`) never leak into the base.
42
+ */
43
+ export declare const splitClassToken: (token: string) => SplitToken;
44
+ export interface ClassListResult {
45
+ value: string;
46
+ changed: boolean;
47
+ }
48
+ /**
49
+ * Rewrite a whitespace-separated class list. Whitespace (including newlines in
50
+ * multi-line `class` attributes) is preserved verbatim; each non-whitespace
51
+ * token is looked up once by its bare utility name. `onRename` / `onReport`
52
+ * fire once per occurrence with the full token as written.
53
+ */
54
+ export declare const rewriteClassList: (value: string, rules: ClassRules, enabled: (id: string) => boolean, onRename: (entry: ClassRenameEntry, before: string, after: string) => void, onReport: (entry: ClassReportEntry, token: string) => void) => ClassListResult;
55
+ export {};
@@ -0,0 +1,91 @@
1
+ import { ruleId } from '../../manifest/registry.js';
2
+ /** Tag used for the manifest-global class rules, which no component owns. */
3
+ const GLOBAL_TAG = 'global';
4
+ const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
5
+ const cache = new WeakMap();
6
+ export const classRulesOf = (manifest) => {
7
+ const cached = cache.get(manifest);
8
+ if (cached)
9
+ return cached;
10
+ const renames = new Map();
11
+ const reports = new Map();
12
+ for (const rule of manifest.global.classes ?? []) {
13
+ if (rule.kind === 'classRename')
14
+ renames.set(rule.from, { rule, id: ruleId(GLOBAL_TAG, rule) });
15
+ else
16
+ reports.set(rule.name, { rule, id: ruleId(GLOBAL_TAG, rule) });
17
+ }
18
+ const names = [...renames.keys(), ...reports.keys()];
19
+ // Word-ish boundaries so a short name like `gap` never matches inside
20
+ // `gap-4` or prose; `-` counts as a word character in utility names.
21
+ const rules = {
22
+ renames,
23
+ reports,
24
+ candidateRe: names.length > 0
25
+ ? new RegExp(`(?<![\\w-])(?:${names.map(escapeRe).join('|')})(?![\\w-])`)
26
+ : null,
27
+ };
28
+ cache.set(manifest, rules);
29
+ return rules;
30
+ };
31
+ export const hasClassRules = (rules) => rules.renames.size > 0 || rules.reports.size > 0;
32
+ /**
33
+ * Split one class token into variant prefix, important markers and the bare
34
+ * utility. The prefix ends at the last `:` outside square brackets, so
35
+ * arbitrary variants (`[&>li]:`, `[@media(hover)]:`) never leak into the base.
36
+ */
37
+ export const splitClassToken = (token) => {
38
+ let depth = 0;
39
+ let cut = -1;
40
+ for (let i = 0; i < token.length; i++) {
41
+ const ch = token[i];
42
+ if (ch === '[')
43
+ depth += 1;
44
+ else if (ch === ']')
45
+ depth = Math.max(0, depth - 1);
46
+ else if (ch === ':' && depth === 0)
47
+ cut = i;
48
+ }
49
+ let rest = token.slice(cut + 1);
50
+ let bang = '';
51
+ let trailingBang = '';
52
+ if (rest.startsWith('!')) {
53
+ bang = '!';
54
+ rest = rest.slice(1);
55
+ }
56
+ if (rest.endsWith('!')) {
57
+ trailingBang = '!';
58
+ rest = rest.slice(0, -1);
59
+ }
60
+ return { prefix: token.slice(0, cut + 1), bang, base: rest, trailingBang };
61
+ };
62
+ /**
63
+ * Rewrite a whitespace-separated class list. Whitespace (including newlines in
64
+ * multi-line `class` attributes) is preserved verbatim; each non-whitespace
65
+ * token is looked up once by its bare utility name. `onRename` / `onReport`
66
+ * fire once per occurrence with the full token as written.
67
+ */
68
+ export const rewriteClassList = (value, rules, enabled, onRename, onReport) => {
69
+ if (!rules.candidateRe || !rules.candidateRe.test(value))
70
+ return { value, changed: false };
71
+ let changed = false;
72
+ const parts = value.split(/(\s+)/);
73
+ for (let i = 0; i < parts.length; i++) {
74
+ const token = parts[i];
75
+ if (token === '' || /\s/.test(token))
76
+ continue;
77
+ const { prefix, bang, base, trailingBang } = splitClassToken(token);
78
+ const rename = rules.renames.get(base);
79
+ if (rename && enabled(rename.id)) {
80
+ const after = `${prefix}${bang}${rename.rule.to}${trailingBang}`;
81
+ parts[i] = after;
82
+ changed = true;
83
+ onRename(rename, token, after);
84
+ continue;
85
+ }
86
+ const report = rules.reports.get(base);
87
+ if (report && enabled(report.id))
88
+ onReport(report, token);
89
+ }
90
+ return { value: changed ? parts.join('') : value, changed };
91
+ };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Offset-based text edits. Surfaces that edit source by character range (HTML
3
+ * via parse5 locations) collect {@link Edit}s and apply them all at once;
4
+ * applying right-to-left keeps earlier offsets valid.
5
+ */
6
+ export interface Edit {
7
+ start: number;
8
+ end: number;
9
+ text: string;
10
+ }
11
+ /** Apply edits to `source`. Non-overlapping edits are assumed; ties break by widest-first. */
12
+ export declare const applyEdits: (source: string, edits: readonly Edit[]) => string;
13
+ /** 1-based line/column for a character offset. */
14
+ export declare const offsetToLineCol: (source: string, offset: number) => {
15
+ line: number;
16
+ column: number;
17
+ };
18
+ /** The leading whitespace of the line containing `offset` (used to indent injected comments). */
19
+ export declare const lineIndentAt: (source: string, offset: number) => string;
@@ -0,0 +1,30 @@
1
+ /** Apply edits to `source`. Non-overlapping edits are assumed; ties break by widest-first. */
2
+ export const applyEdits = (source, edits) => {
3
+ const sorted = [...edits].sort((a, b) => b.start - a.start || b.end - a.end);
4
+ let out = source;
5
+ for (const edit of sorted)
6
+ out = out.slice(0, edit.start) + edit.text + out.slice(edit.end);
7
+ return out;
8
+ };
9
+ /** 1-based line/column for a character offset. */
10
+ export const offsetToLineCol = (source, offset) => {
11
+ let line = 1;
12
+ let lineStart = 0;
13
+ for (let i = 0; i < offset && i < source.length; i++) {
14
+ if (source[i] === '\n') {
15
+ line++;
16
+ lineStart = i + 1;
17
+ }
18
+ }
19
+ return { line, column: offset - lineStart + 1 };
20
+ };
21
+ /** The leading whitespace of the line containing `offset` (used to indent injected comments). */
22
+ export const lineIndentAt = (source, offset) => {
23
+ let start = offset;
24
+ while (start > 0 && source[start - 1] !== '\n')
25
+ start--;
26
+ let end = start;
27
+ while (end < source.length && (source[end] === ' ' || source[end] === '\t'))
28
+ end++;
29
+ return source.slice(start, end);
30
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Build the logical negation of an expression's source text, with light,
3
+ * always-safe simplification. When in doubt the expression is wrapped in
4
+ * parentheses — over-parenthesising never changes meaning.
5
+ *
6
+ * negateExprText('x') === '!x'
7
+ * negateExprText('a && b') === '!(a && b)'
8
+ * negateExprText('!x') === 'x'
9
+ * negateExprText('!(a && b)') === '(a && b)'
10
+ * negateExprText('true') === 'false'
11
+ * negateExprText('foo.bar()') === '!foo.bar()'
12
+ */
13
+ export declare const negateExprText: (expr: string) => string;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Build the logical negation of an expression's source text, with light,
3
+ * always-safe simplification. When in doubt the expression is wrapped in
4
+ * parentheses — over-parenthesising never changes meaning.
5
+ *
6
+ * negateExprText('x') === '!x'
7
+ * negateExprText('a && b') === '!(a && b)'
8
+ * negateExprText('!x') === 'x'
9
+ * negateExprText('!(a && b)') === '(a && b)'
10
+ * negateExprText('true') === 'false'
11
+ * negateExprText('foo.bar()') === '!foo.bar()'
12
+ */
13
+ export const negateExprText = (expr) => {
14
+ const t = expr.trim();
15
+ if (t === 'true')
16
+ return 'false';
17
+ if (t === 'false')
18
+ return 'true';
19
+ // Strip a leading `!` when it negates the whole expression (double negation).
20
+ if (t.startsWith('!')) {
21
+ const rest = t.slice(1).trim();
22
+ if (isAtomic(rest) || isFullyParenthesized(rest))
23
+ return rest;
24
+ }
25
+ return isAtomic(t) || isFullyParenthesized(t) ? `!${t}` : `!(${t})`;
26
+ };
27
+ /**
28
+ * A simple expression with no top-level operators: an identifier optionally
29
+ * followed by member access, calls or index chains (e.g. `a`, `a.b`, `a.b()`,
30
+ * `a[0].b`). These can be negated with a bare `!` prefix.
31
+ */
32
+ const ATOMIC = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\([^()]*\)|\[[^[\]]*\])*$/;
33
+ const isAtomic = (s) => ATOMIC.test(s.trim());
34
+ /** True when the string is a single parenthesised group spanning its whole length, e.g. `(a && b)`. */
35
+ const isFullyParenthesized = (s) => {
36
+ const t = s.trim();
37
+ if (t.length < 2 || t[0] !== '(' || t[t.length - 1] !== ')')
38
+ return false;
39
+ let depth = 0;
40
+ for (let i = 0; i < t.length; i++) {
41
+ if (t[i] === '(')
42
+ depth++;
43
+ else if (t[i] === ')') {
44
+ depth--;
45
+ // Closed the opening paren before the end → not a single enclosing group.
46
+ if (depth === 0 && i !== t.length - 1)
47
+ return false;
48
+ }
49
+ }
50
+ return depth === 0;
51
+ };
@@ -0,0 +1,17 @@
1
+ /** Shared transformer contract used by every surface. */
2
+ import { type Finding } from '../../report/types.js';
3
+ export interface TransformContext {
4
+ /** Path of the file being transformed (for findings). */
5
+ file: string;
6
+ /** When set, only these rule ids run. */
7
+ only?: ReadonlySet<string>;
8
+ /** These rule ids are skipped. */
9
+ skip?: ReadonlySet<string>;
10
+ }
11
+ export interface TransformResult {
12
+ output: string;
13
+ changed: boolean;
14
+ findings: Finding[];
15
+ }
16
+ /** Whether a rule id passes the `--only` / `--skip` filters. */
17
+ export declare const ruleEnabled: (ctx: TransformContext, id: string) => boolean;
@@ -0,0 +1,8 @@
1
+ /** Whether a rule id passes the `--only` / `--skip` filters. */
2
+ export const ruleEnabled = (ctx, id) => {
3
+ if (ctx.only && !ctx.only.has(id))
4
+ return false;
5
+ if (ctx.skip && ctx.skip.has(id))
6
+ return false;
7
+ return true;
8
+ };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Surface-agnostic model of an attribute / prop value.
3
+ *
4
+ * Every surface (HTML, React, Angular) parses its own syntax into one of these
5
+ * shapes so the shared transformation core (`attribute-ops`) can reason about
6
+ * values without knowing whether it is looking at `arrow`, `arrow={x}` or
7
+ * `[arrow]="x"`.
8
+ */
9
+ export type AttrValue =
10
+ /** Shorthand presence: HTML `arrow`, JSX `arrow`, Angular `arrow` → logically `true`. */
11
+ {
12
+ kind: 'bareTrue';
13
+ }
14
+ /** A boolean literal: JSX `arrow={true}`, Angular `[arrow]="false"`. */
15
+ | {
16
+ kind: 'boolLiteral';
17
+ value: boolean;
18
+ }
19
+ /** A static string value: HTML `tone="ghost"`, JSX `tone="ghost"`. */
20
+ | {
21
+ kind: 'stringLiteral';
22
+ value: string;
23
+ }
24
+ /** A dynamic expression whose text is preserved verbatim: JSX `arrow={x && y}`, Angular `[arrow]="x && y"`. */
25
+ | {
26
+ kind: 'dynamic';
27
+ expr: string;
28
+ };
29
+ export declare const bareTrue: () => AttrValue;
30
+ export declare const boolLiteral: (value: boolean) => AttrValue;
31
+ export declare const stringLiteral: (value: string) => AttrValue;
32
+ export declare const dynamic: (expr: string) => AttrValue;
33
+ /** True when the value is known at codemod time (not a runtime expression). */
34
+ export declare const isStatic: (value: AttrValue) => boolean;
35
+ /**
36
+ * Coerce a value to a boolean interpretation when the prop is boolean-typed.
37
+ * Returns the literal boolean, or the raw expression text when dynamic.
38
+ *
39
+ * String semantics follow Stencil's boolean prop parsing: the string `"false"`
40
+ * is `false`, every other present string is `true`.
41
+ */
42
+ export type ResolvedBool = {
43
+ kind: 'literal';
44
+ value: boolean;
45
+ } | {
46
+ kind: 'expr';
47
+ expr: string;
48
+ };
49
+ export declare const resolveBoolean: (value: AttrValue) => ResolvedBool;
@@ -0,0 +1,18 @@
1
+ export const bareTrue = () => ({ kind: 'bareTrue' });
2
+ export const boolLiteral = (value) => ({ kind: 'boolLiteral', value });
3
+ export const stringLiteral = (value) => ({ kind: 'stringLiteral', value });
4
+ export const dynamic = (expr) => ({ kind: 'dynamic', expr });
5
+ /** True when the value is known at codemod time (not a runtime expression). */
6
+ export const isStatic = (value) => value.kind !== 'dynamic';
7
+ export const resolveBoolean = (value) => {
8
+ switch (value.kind) {
9
+ case 'bareTrue':
10
+ return { kind: 'literal', value: true };
11
+ case 'boolLiteral':
12
+ return { kind: 'literal', value: value.value };
13
+ case 'stringLiteral':
14
+ return { kind: 'literal', value: value.value !== 'false' };
15
+ case 'dynamic':
16
+ return { kind: 'expr', expr: value.expr };
17
+ }
18
+ };
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@maggioli-design-system/magma-codemods",
3
+ "version": "2.0.0-beta.0",
4
+ "description": "Codemods to migrate consumer code (HTML, React/JSX, Angular templates, CSS/SCSS) of @maggioli-design-system/magma from v1 to v2.",
5
+ "license": "ISC",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=22"
9
+ },
10
+ "bin": {
11
+ "magma-codemods": "dist/cli.js"
12
+ },
13
+ "main": "dist/index.js",
14
+ "types": "dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist/"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "scripts": {
28
+ "build": "tsc -p tsconfig.json",
29
+ "generate.candidate": "tsx scripts/build-candidate-manifest.ts",
30
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest",
31
+ "lint": "eslint ."
32
+ },
33
+ "dependencies": {
34
+ "@angular/compiler": "20.3.30",
35
+ "arg": "5.0.2",
36
+ "chalk": "5.6.2",
37
+ "globby": "14.1.0",
38
+ "parse5": "7.3.0",
39
+ "postcss": "8.5.28",
40
+ "postcss-scss": "4.0.9",
41
+ "ts-morph": "28.0.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/jest": "29.5.14",
45
+ "jest": "29.7.0",
46
+ "ts-jest": "29.2.5"
47
+ },
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/magma-design-system/magma.git",
51
+ "directory": "projects/codemod"
52
+ },
53
+ "keywords": [
54
+ "codemod",
55
+ "migration",
56
+ "magma",
57
+ "design-system",
58
+ "jscodeshift",
59
+ "stencil"
60
+ ]
61
+ }