Skip to content

Commit c24e650

Browse files
committed
feat(data-mapper-v2): Make XSLT the source of truth for map definitions
This change fundamentally shifts how Data Mapper v2 stores and loads maps: **Core Changes:** - XSLT is now the authoritative source for map definitions - Added XsltParser to extract map definitions from XSLT text value templates - Added XsltMetadataSerializer for v3 metadata format (UI layout only) - Removed LML embedding in XSLT comments (v2 format deprecated) **XPath to LML Conversion:** - Convert single-quoted string literals to double quotes for LML compatibility - Convert XPath infix math operators (+, -, *, div, mod) to function calls - Remove namespace prefixes (math:, xs:, etc.) from function names - Flatten associative operators (+, *) to multi-argument function calls **Bug Fixes:** - Fix delete button not working for custom values in unbounded inputs - Fix node position changes not triggering dirty state (Save button) **VS Code Extension:** - Update file service to use XSLT content directly - Add commands for loading/saving XSLT content
1 parent 70d9a7d commit c24e650

29 files changed

Lines changed: 2724 additions & 189 deletions

File tree

Localize/lang/strings.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3810,6 +3810,7 @@
38103810
"_qJpnIL.comment": "Label for description of custom endsWith Function",
38113811
"_qKVOwV.comment": "Placeholder text for the MCP server name field",
38123812
"_qMFpNH.comment": "Loading dynamic data",
3813+
"_qNy7WP.comment": "Message to display when XSLT content hasn't been generated yet",
38133814
"_qSejoi.comment": "Label for description of custom lessOrEquals Function",
38143815
"_qSt0Sb.comment": "Accessibility prefix for the input label",
38153816
"_qUWBUX.comment": "A duration of time shown in days",
@@ -4097,7 +4098,7 @@
40974098
"_wQcEXt.comment": "Required parameters for the custom Replace Function",
40984099
"_wQsEwc.comment": "Required length parameter to obtain substring",
40994100
"_wT/gMB.comment": "Description for featured connectors field",
4100-
"_wTaSTp.comment": "Tooltip for disabled test button",
4101+
"_wTaSTp.comment": "Message when XSLT content is not available",
41014102
"_wV3Lmd.comment": "Label to delete dictionary item",
41024103
"_wWVQuK.comment": "Label for description of custom if Function",
41034104
"_wWuzqz.comment": "Ok button text",
@@ -4919,6 +4920,7 @@
49194920
"qJpnIL": "Checks if the string ends with a value (case-insensitive, invariant culture)",
49204921
"qKVOwV": "Enter a name for the MCP server",
49214922
"qMFpNH": "Loading dynamic data",
4923+
"qNy7WP": "XSLT not yet generated. Save the map to generate XSLT output.",
49224924
"qSejoi": "Returns true if the first argument is less than or equal to the second",
49234925
"qSt0Sb": "Required",
49244926
"qUWBUX": "{days, plural, one {# day} other {# days}}",

apps/vs-code-designer/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,14 @@
4242
"process-tree": "^1.0.3",
4343
"ps-tree": "^1.2.0",
4444
"recursive-copy": "^2.0.14",
45+
"saxon-js": "^2.6.0",
4546
"semver": "^6.3.1",
4647
"tslib": "2.4.0",
4748
"uuid": "^10.0.0",
4849
"vscode-extension-tester": "^8.17.0",
4950
"vscode-nls": "^5.2.0",
5051
"xml2js": "0.6.2",
52+
"xslt3": "^2.7.0",
5153
"yaml": "^2.7.0",
5254
"yaml-types": "^0.4.0",
5355
"yargs-parser": "21.1.1",

apps/vs-code-designer/src/app/commands/dataMapper/DataMapperExt.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,63 @@ import { parse } from 'yaml';
1515
import { localize } from '../../../localize';
1616
import { assetsFolderName, dataMapNameValidation } from '../../../constants';
1717

18+
/**
19+
* UI metadata for function positions and canvas state.
20+
*/
21+
interface XsltUiMetadata {
22+
functionNodes: Array<{
23+
reactFlowGuid: string;
24+
functionKey: string;
25+
position: { x: number; y: number };
26+
connections: Array<{ name: string; inputOrder: number }>;
27+
connectionShorthand: string;
28+
}>;
29+
canvasRect?: { x: number; y: number; width: number; height: number };
30+
}
31+
32+
/**
33+
* Metadata embedded in XSLT files for Data Mapper v2.
34+
* Contains mapDefinition in the metadata (legacy format).
35+
* @deprecated Use XsltMapMetadataV3 which doesn't embed mapDefinition
36+
*/
37+
interface XsltMapMetadataV2 {
38+
version: '2.0';
39+
sourceSchema: string;
40+
targetSchema: string;
41+
mapDefinition: MapDefinitionEntry;
42+
metadata: XsltUiMetadata;
43+
}
44+
45+
/**
46+
* Metadata embedded in XSLT files for Data Mapper v3.
47+
* Only embeds layout metadata - mapping logic is derived from XSLT content.
48+
*/
49+
interface XsltMapMetadataV3 {
50+
version: '3.0';
51+
sourceSchema: string;
52+
targetSchema: string;
53+
metadata: XsltUiMetadata;
54+
}
55+
56+
/**
57+
* Union type supporting both v2 and v3 metadata formats.
58+
*/
59+
type XsltMapMetadata = XsltMapMetadataV2 | XsltMapMetadataV3;
60+
61+
/**
62+
* Type guard to check if metadata is v2 format (with mapDefinition).
63+
*/
64+
const isV2Metadata = (metadata: XsltMapMetadata): metadata is XsltMapMetadataV2 => {
65+
return metadata.version === '2.0' && 'mapDefinition' in metadata;
66+
};
67+
68+
/**
69+
* Type guard to check if metadata is v3 format (without mapDefinition).
70+
*/
71+
const isV3Metadata = (metadata: XsltMapMetadata): metadata is XsltMapMetadataV3 => {
72+
return metadata.version === '3.0';
73+
};
74+
1875
export default class DataMapperExt {
1976
public static async openDataMapperPanel(
2077
context: IActionContext,
@@ -119,4 +176,101 @@ export default class DataMapperExt {
119176
}
120177
}
121178
}
179+
180+
/**
181+
* Regex to extract metadata JSON from XSLT comment.
182+
*/
183+
private static readonly METADATA_REGEX = /<!--\s*LogicAppsDataMapper:\s*([\s\S]*?)\s*-->/;
184+
185+
/**
186+
* Checks if an XSLT string has embedded metadata.
187+
*/
188+
public static hasEmbeddedMetadata(xslt: string): boolean {
189+
return DataMapperExt.METADATA_REGEX.test(xslt);
190+
}
191+
192+
/**
193+
* Extracts metadata from an XSLT string if present.
194+
* Supports both v2 (with mapDefinition) and v3 (without mapDefinition) formats.
195+
*/
196+
public static extractMetadataFromXslt(xslt: string): XsltMapMetadata | null {
197+
const match = xslt.match(DataMapperExt.METADATA_REGEX);
198+
199+
if (!match || !match[1]) {
200+
return null;
201+
}
202+
203+
try {
204+
const metadata = JSON.parse(match[1]) as XsltMapMetadata;
205+
206+
// Validate required fields (common to both v2 and v3)
207+
if (!metadata.version || !metadata.sourceSchema || !metadata.targetSchema) {
208+
console.warn('XSLT metadata missing required fields');
209+
return null;
210+
}
211+
212+
// v2 requires mapDefinition, v3 does not
213+
if (metadata.version === '2.0' && !('mapDefinition' in metadata)) {
214+
console.warn('XSLT metadata v2.0 missing mapDefinition');
215+
return null;
216+
}
217+
218+
return metadata;
219+
} catch (error) {
220+
console.error('Failed to parse XSLT metadata JSON:', error);
221+
return null;
222+
}
223+
}
224+
225+
/**
226+
* Return type for loadMapFromXslt
227+
*/
228+
public static loadMapFromXslt(
229+
xsltContent: string,
230+
_extInstance: typeof ext
231+
): {
232+
mapDefinition: MapDefinitionEntry;
233+
sourceSchemaFileName: string;
234+
targetSchemaFileName: string;
235+
metadata?: XsltUiMetadata;
236+
xsltContent?: string;
237+
isV3Format?: boolean;
238+
} | null {
239+
const metadata = DataMapperExt.extractMetadataFromXslt(xsltContent);
240+
241+
if (!metadata) {
242+
return null;
243+
}
244+
245+
// Handle v2 format (with embedded mapDefinition)
246+
if (isV2Metadata(metadata)) {
247+
// Fix custom values in the map definition (same processing as LML)
248+
DataMapperExt.fixMapDefinitionCustomValues(metadata.mapDefinition);
249+
250+
return {
251+
mapDefinition: metadata.mapDefinition,
252+
sourceSchemaFileName: metadata.sourceSchema,
253+
targetSchemaFileName: metadata.targetSchema,
254+
metadata: metadata.metadata,
255+
isV3Format: false,
256+
};
257+
}
258+
259+
// Handle v3 format (without embedded mapDefinition)
260+
// For v3, we pass the raw XSLT content so the webview can parse it
261+
// to derive the mapping connections from the actual XSLT
262+
if (isV3Metadata(metadata)) {
263+
return {
264+
// Empty mapDefinition - webview will derive from XSLT
265+
mapDefinition: {},
266+
sourceSchemaFileName: metadata.sourceSchema,
267+
targetSchemaFileName: metadata.targetSchema,
268+
metadata: metadata.metadata,
269+
xsltContent: xsltContent,
270+
isV3Format: true,
271+
};
272+
}
273+
274+
return null;
275+
}
122276
}

0 commit comments

Comments
 (0)