diff --git a/packages/language-service/src/language-plugin.ts b/packages/language-service/src/language-plugin.ts index 92b43cd..abc0fc6 100644 --- a/packages/language-service/src/language-plugin.ts +++ b/packages/language-service/src/language-plugin.ts @@ -213,36 +213,64 @@ function* getTempblotEmbeddedCodes( return; } - // Create JSON output with interpolations replaced by placeholder values - const { transformedText, jsonMappings } = createJsonWithMappings( + const outputLanguageId = getOutputLanguageId(output.attributes.lang); + const { transformedText, mappings } = createOutputWithMappings( outputText, interpolationsData, output.startTagEnd, + outputLanguageId, ); - // TODO: Make generic and not tied to JSON yield { - id: "output_json", - languageId: "json", + id: "output", + languageId: outputLanguageId, snapshot: { getText: (start, end) => transformedText.substring(start, end), getLength: () => transformedText.length, getChangeRange: () => undefined, }, - mappings: jsonMappings, + mappings, embeddedCodes: [], }; } -function createJsonWithMappings( +function getOutputLanguageId(lang: string | undefined): string { + switch (lang) { + case undefined: + case "html": + return "html"; + case "md": + return "markdown"; + case "js": + return "javascript"; + case "jsx": + return "javascriptreact"; + case "ts": + return "typescript"; + case "tsx": + return "typescriptreact"; + case "txt": + return "plaintext"; + case "gql": + return "graphql"; + case "coffee": + return "coffeescript"; + default: + return lang; + } +} + +function createOutputWithMappings( outputText: string, interpolationsData: InterpolationData[], outputStartOffset: number, -): { transformedText: string; jsonMappings: CodeMapping[] } { + languageId: string, +): { transformedText: string; mappings: CodeMapping[] } { const mappings: CodeMapping[] = []; let transformedText = ""; let lastOffset = 0; let generatedOffset = 0; + const interpolationPlaceholder = getInterpolationPlaceholder(languageId); // Process each interpolation for (const interp of interpolationsData) { @@ -266,10 +294,8 @@ function createJsonWithMappings( generatedOffset += beforeText.length; } - // Replace interpolation with null placeholder for JSON validity - const placeholder = "null"; - transformedText += placeholder; - generatedOffset += placeholder.length; + transformedText += interpolationPlaceholder; + generatedOffset += interpolationPlaceholder.length; lastOffset = interp.fullEnd; } @@ -293,5 +319,24 @@ function createJsonWithMappings( transformedText += remainingText; } - return { transformedText, jsonMappings: mappings }; + return { transformedText, mappings }; +} + +function getInterpolationPlaceholder(languageId: string): string { + switch (languageId) { + case "json": + case "jsonc": + case "json5": + return "null"; + case "css": + case "scss": + case "less": + case "sass": + case "stylus": + case "postcss": + case "toml": + return "0"; + default: + return "tempblot"; + } } diff --git a/packages/language-service/tests/language-plugin.spec.ts b/packages/language-service/tests/language-plugin.spec.ts index 08d7192..08e074a 100644 --- a/packages/language-service/tests/language-plugin.spec.ts +++ b/packages/language-service/tests/language-plugin.spec.ts @@ -38,7 +38,7 @@ const value: number = 1; expect(combinedContext?.snapshot.getText(0, combinedContext.snapshot.getLength())) .toContain("const value: number = 1;"); expect(virtualCode.embeddedCodes.map((code) => code.id)).not.toContain( - "output_json", + "output", ); }); @@ -51,16 +51,49 @@ test("creates output embedded code for output-only files", () => { const combinedContext = virtualCode.embeddedCodes.find( (code) => code.id === "combined_context", ); - const outputJson = virtualCode.embeddedCodes.find( - (code) => code.id === "output_json", + const output = virtualCode.embeddedCodes.find( + (code) => code.id === "output", ); expect(combinedContext?.snapshot.getText(0, combinedContext.snapshot.getLength())) .toContain("(1 + 1);"); - expect(outputJson?.snapshot.getText(0, outputJson.snapshot.getLength())) + expect(output?.languageId).toBe("json"); + expect(output?.snapshot.getText(0, output.snapshot.getLength())) .toContain('{"value": null}'); }); +test("creates output embedded code from non-JSON output languages", () => { + const source = ` +

<></h1> +</output>`; + + const virtualCode = createVirtualCode(source); + const output = virtualCode.embeddedCodes.find((code) => code.id === "output"); + + expect(output?.languageId).toBe("html"); + expect(output?.snapshot.getText(0, output.snapshot.getLength())) + .toContain("<h1>tempblot</h1>"); +}); + +test("normalizes output language aliases", () => { + const markdownCode = createVirtualCode( + `<output lang="md"># <<title>></output>`, + ); + const javascriptCode = createVirtualCode( + `<output lang="js">const value = <<value>>;</output>`, + ); + + const markdownOutput = markdownCode.embeddedCodes.find( + (code) => code.id === "output", + ); + const javascriptOutput = javascriptCode.embeddedCodes.find( + (code) => code.id === "output", + ); + + expect(markdownOutput?.languageId).toBe("markdown"); + expect(javascriptOutput?.languageId).toBe("javascript"); +}); + test("creates empty TypeScript embedded code for output-only files without interpolations", () => { const source = `<output lang="json"> { @@ -72,14 +105,14 @@ test("creates empty TypeScript embedded code for output-only files without inter const combinedContext = virtualCode.embeddedCodes.find( (code) => code.id === "combined_context", ); - const outputJson = virtualCode.embeddedCodes.find( - (code) => code.id === "output_json", + const output = virtualCode.embeddedCodes.find( + (code) => code.id === "output", ); expect(combinedContext?.snapshot.getText(0, combinedContext.snapshot.getLength())) .toBe("export {}; // Make this file a module\n\n"); expect(combinedContext?.mappings).toEqual([]); - expect(outputJson?.snapshot.getText(0, outputJson.snapshot.getLength())) + expect(output?.snapshot.getText(0, output.snapshot.getLength())) .toContain('"test": "a"'); });