diff --git a/blobl-editor/wasm/main.go b/blobl-editor/wasm/main.go
index 6477a60c..5a5fd016 100644
--- a/blobl-editor/wasm/main.go
+++ b/blobl-editor/wasm/main.go
@@ -10,7 +10,12 @@ import (
_ "github.com/redpanda-data/connect/v4/public/components/pure/extended"
)
+var globalEnv *bloblang.Environment
+
func main() {
+ // Initialize the global Bloblang environment
+ globalEnv = createMockEnvironment()
+
js.Global().Set("blobl", js.FuncOf(blobl))
// Wait for a signal to shut down
@@ -18,44 +23,196 @@ func main() {
}
func blobl(_ js.Value, args []js.Value) any {
- if len(args) != 2 {
- return fmt.Sprintf("Expected two arguments, received %d instead", len(args))
+ if len(args) < 2 || len(args) > 3 {
+ return fmt.Sprintf("Expected 2 or 3 arguments, received %d instead", len(args))
}
- mapping, err := bloblang.NewEnvironment().Parse(args[0].String())
+ // Parse the mapping
+ mapping, err := globalEnv.Parse(args[0].String())
if err != nil {
return fmt.Sprintf("Failed to parse mapping: %s", err)
}
- msg, err := service.NewMessage([]byte(args[1].String())).BloblangQuery(mapping)
+ // Parse the payload JSON
+ var payload map[string]any
+ if err := json.Unmarshal([]byte(args[1].String()), &payload); err != nil {
+ return fmt.Sprintf("Failed to parse payload: %s", err)
+ }
+
+ // Parse the optional metadata
+ metadata := map[string]any{}
+ if len(args) == 3 {
+ if err := json.Unmarshal([]byte(args[2].String()), &metadata); err != nil {
+ return fmt.Sprintf("Failed to parse metadata: %s", err)
+ }
+ }
+
+
+ // Serialize the payload for the message
+ payloadBytes, err := json.Marshal(payload)
+ if err != nil {
+ return fmt.Sprintf("Failed to serialize payload: %s", err)
+ }
+
+ // Create a new message with the payload
+ msg := service.NewMessage(payloadBytes)
+
+ // Apply metadata to the message
+ for key, value := range metadata {
+ strValue, ok := value.(string)
+ if !ok {
+ return fmt.Errorf("metadata value for key '%s' must be a string, got %T", key, value)
+ }
+ msg.MetaSet(key, strValue)
+ }
+
+ // Execute the mapping
+ result, err := msg.BloblangQuery(mapping)
if err != nil {
return fmt.Sprintf("Failed to execute mapping: %s", err)
}
- message, err := msg.AsStructured()
+ // Extract the structured message
+ message, err := result.AsStructured()
if err != nil {
return fmt.Sprintf("Failed to marshal message: %s", err)
}
- var metadata map[string]any
- msg.MetaWalkMut(func(key string, value any) error {
- if metadata == nil {
- metadata = make(map[string]any)
+ // Extract metadata
+ var extractedMetadata map[string]any
+ result.MetaWalkMut(func(key string, value any) error {
+ if extractedMetadata == nil {
+ extractedMetadata = make(map[string]any)
}
- metadata[key] = value
+ extractedMetadata[key] = value
return nil
})
+ // Marshal the final output
var output []byte
if output, err = json.MarshalIndent(struct {
Msg any `json:"msg"`
Meta map[string]any `json:"meta,omitempty"`
}{
Msg: message,
- Meta: metadata,
+ Meta: extractedMetadata,
}, "", " "); err != nil {
return fmt.Sprintf("Failed to marshal output: %s", err)
}
return string(output)
}
+
+// createMockEnvironment creates a shared Bloblang environment with mocked I/O functions.
+func createMockEnvironment() *bloblang.Environment {
+ env := bloblang.NewEnvironment()
+
+ // Mock `env` function
+ env.RegisterFunction("env", func(args ...any) (bloblang.Function, error) {
+ return func() (any, error) {
+ var name string
+ var noCache bool
+
+ if len(args) == 1 {
+ name, _ = args[0].(string)
+ } else if len(args) == 2 {
+ switch v := args[0].(type) {
+ case string:
+ name = v
+ noCache, _ = args[1].(bool)
+ case map[string]any:
+ name, _ = v["name"].(string)
+ noCache, _ = v["no_cache"].(bool)
+ default:
+ return nil, fmt.Errorf("invalid argument format for `env`")
+ }
+ } else {
+ return nil, fmt.Errorf("invalid number of arguments for `env`")
+ }
+
+ mockValues := map[string]string{
+ "key": "mocked_value",
+ }
+ if val, exists := mockValues[name]; exists {
+ if noCache {
+ return val + " (no cache)", nil
+ }
+ return val, nil
+ }
+ return nil, nil
+ }, nil
+ })
+
+ // Mock `file` function
+ env.RegisterFunction("file", func(args ...any) (bloblang.Function, error) {
+ return func() (any, error) {
+ var path string
+ var noCache bool
+
+ if len(args) == 1 {
+ path, _ = args[0].(string)
+ } else if len(args) == 2 {
+ params, ok := args[0].(map[string]any)
+ if !ok {
+ return nil, fmt.Errorf("invalid argument format for `file`")
+ }
+ path, _ = params["path"].(string)
+ noCache, _ = params["no_cache"].(bool)
+ } else {
+ return nil, fmt.Errorf("invalid number of arguments for `file`")
+ }
+
+ mockFiles := map[string]string{
+ "/mock/path/file.json": `{"hello": "world"}`,
+ }
+ if content, exists := mockFiles[path]; exists {
+ if noCache {
+ return content + " (no_cache)", nil
+ }
+ return content, nil
+ }
+ return nil, fmt.Errorf("file not found: %s", path)
+ }, nil
+ })
+
+ // Mock `file_rel` function
+ env.RegisterFunction("file_rel", func(args ...any) (bloblang.Function, error) {
+ return func() (any, error) {
+ var path string
+ var noCache bool
+
+ if len(args) == 1 {
+ path, _ = args[0].(string)
+ } else if len(args) == 2 {
+ params, ok := args[0].(map[string]any)
+ if !ok {
+ return nil, fmt.Errorf("invalid argument format for `file_rel`")
+ }
+ path, _ = params["path"].(string)
+ noCache, _ = params["no_cache"].(bool)
+ } else {
+ return nil, fmt.Errorf("invalid number of arguments for `file_rel`")
+ }
+
+ mockFiles := map[string]string{
+ "relative/path/file.json": `{"hello": "world"}`,
+ }
+ if content, exists := mockFiles[path]; exists {
+ if noCache {
+ return content + " (no_cache)", nil
+ }
+ return content, nil
+ }
+ return nil, fmt.Errorf("file not found: %s", path)
+ }, nil
+ })
+
+ // Mock `hostname` function
+ env.RegisterFunction("hostname", func(args ...any) (bloblang.Function, error) {
+ return func() (any, error) {
+ return "mocked-hostname", nil
+ }, nil
+ })
+
+ return env
+}
diff --git a/src/css/bloblang-playground.css b/src/css/bloblang-playground.css
index 5870c88d..ce374327 100644
--- a/src/css/bloblang-playground.css
+++ b/src/css/bloblang-playground.css
@@ -127,10 +127,6 @@ html[data-theme=dark] .bloblang-playground button[type=submit]:not(.aa-SubmitBut
margin: 0;
}
-.bloblang-playground .output-section .editor-container details {
- height: 100%;
-}
-
.bloblang-playground .editor-container summary {
padding: 10px;
cursor: pointer;
@@ -145,27 +141,31 @@ html[data-theme=dark] .bloblang-playground button[type=submit]:not(.aa-SubmitBut
/* Editor areas */
.bloblang-playground .editor {
- font-size: 12pt;
+ font-size: var(--body-font-size);
background-color: var(--pre-background) !important;
color: var(--code-font-color) !important;
- height: 250px;
+ min-height: 130px;
padding: 10px;
overflow-y: auto;
+ border-radius: 5px;
position: relative;
border: 1px solid rgb(204, 204, 204);
- resize: both;
+ resize: horizontal;
min-width: 200px;
- min-height: 100px;
}
/* Output section (right side) */
.bloblang-playground .output-section {
- flex: 1 1 35%;
+ flex: 1 1 45%;
gap: 20px;
display: flex;
flex-direction: column;
}
+.bloblang-playground #ace-input-metadata {
+ min-height: unset;
+}
+
.bloblang-playground .choices[data-type*=select-one] .choices__inner {
padding: 0;
}
@@ -213,6 +213,46 @@ html[data-theme=dark] .bloblang-playground button[type=submit]:not(.aa-SubmitBut
gap: 5px;
}
+.bloblang-snippet {
+ display: flex;
+ flex-direction: column;
+ gap: 1em;
+ padding: 1.5em;
+ border: 1px solid rgb(204, 204, 204);
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ margin: 2em 0;
+}
+
+.bloblang-snippet .bloblang-editor {
+ font-family: var(--monospace-font-family);
+ font-size: var(--body-font-size);
+ resize: horizontal;
+}
+
+.bloblang-snippet .row {
+ display: flex;
+ gap: 1em;
+ flex-wrap: wrap;
+}
+
+.bloblang-snippet .box {
+ flex: 1;
+}
+
+.bloblang-snippet .box.full-width {
+ flex: 0 0 100%;
+}
+
+.bloblang-snippet .ace-editor {
+ width: 100%;
+ border: 1px solid rgb(204, 204, 204);
+ border-radius: 5px;
+ min-height: 75px;
+ color: var(--code-font-color);
+ background-color: var(--pre-background);
+}
+
@media (max-width: 1024px) {
.bloblang-playground .banner {
margin-left: -1rem;
@@ -244,4 +284,8 @@ html[data-theme=dark] .bloblang-playground button[type=submit]:not(.aa-SubmitBut
.bloblang-playground .doc {
margin-right: unset;
}
+
+ .bloblang-snippet .row {
+ flex-direction: column;
+ }
}
diff --git a/src/partials/bloblang-playground.hbs b/src/partials/bloblang-playground.hbs
index c51dc4f5..cef6bcb7 100644
--- a/src/partials/bloblang-playground.hbs
+++ b/src/partials/bloblang-playground.hbs
@@ -17,9 +17,13 @@
- Input (JSON)
+ Input
+
+ Input metadata
+
+
@@ -34,11 +38,11 @@
Output
- "Output will appear here..."
+ "Output will appear here..."
- Metadata output
- "Metadata will appear here..."
+ Output metadata
+ "Metadata will appear here..."
@@ -58,6 +62,7 @@ const TAB_SIZE = 2;
// Keys for sessionStorage
const sessionStorageKeys = {
input: "blobl-editor-input",
+ metadata: "blobl-editor-metadata",
mapping: "blobl-editor-mapping",
};
@@ -65,10 +70,35 @@ const sessionStorageKeys = {
const defaultInput = `{
"numbers": [1, 2, 3, 4, 5]
}`;
+const defaultMetaInput = "{}";
const defaultMapping = `root.even_numbers = this.numbers.filter(n -> n % 2 == 0)
root.sum = this.numbers.sum()`;
const defaultOutput = "Output will appear here...";
-const defaultMeta = "Output metadata will appear here...";
+const defaultMetaOutput = "Output metadata will appear here...";
+
+// Initialize ACE Editors
+function initializeAceEditor(editorId, mode, readOnly = false, initialValue = '') {
+ const editor = ace.edit(editorId);
+ editor.setTheme('ace/theme/github');
+ editor.session.setMode(mode);
+ editor.setReadOnly(readOnly);
+ editor.setValue(prettifyJSON(initialValue), 1);
+ editor.session.setTabSize(TAB_SIZE);
+ editor.session.setUseSoftTabs(true);
+ editor.setOptions({
+ minLines: 1, // Minimum height
+ maxLines: 50, // Allow growth
+ });
+ return editor;
+}
+
+function prettifyJSON(json) {
+ try {
+ return JSON.stringify(JSON.parse(json), null, 2);
+ } catch (error) {
+ return json; // Return original value if it's not valid JSON
+ }
+}
document.addEventListener("DOMContentLoaded", () => {
metadataDetails = document.getElementById("metadata-details");
@@ -80,35 +110,19 @@ document.addEventListener("DOMContentLoaded", () => {
}
// Initialize input editor
- aceInputEditor = ace.edit("ace-input");
- aceInputEditor.setTheme("ace/theme/github");
- aceInputEditor.session.setMode("ace/mode/json");
- aceInputEditor.session.setTabSize(TAB_SIZE);
- aceInputEditor.session.setUseSoftTabs(true);
+ aceInputEditor = initializeAceEditor("ace-input", 'ace/mode/json', false, defaultInput);
+
+ // Initialize input metadata editor
+ aceInputMetadataEditor = initializeAceEditor("ace-input-metadata", 'ace/mode/json', false, defaultMetaInput);
// Initialize mapping editor
- aceMappingEditor = ace.edit("ace-mapping");
- aceMappingEditor.setTheme("ace/theme/github");
- aceMappingEditor.session.setMode("ace/mode/coffee");
- aceMappingEditor.session.setOption("useWorker", false);
- aceMappingEditor.session.setTabSize(TAB_SIZE);
- aceMappingEditor.session.setUseSoftTabs(true);
+ aceMappingEditor = initializeAceEditor("ace-mapping", 'ace/mode/coffee', false, defaultMapping);
// Initialize output editor
- aceOutputEditor = ace.edit("ace-output");
- aceOutputEditor.setTheme("ace/theme/github");
- aceOutputEditor.session.setMode("ace/mode/text"); // JSON syntax highlighting
- aceOutputEditor.setReadOnly(true); // Make output read-only
- aceOutputEditor.session.setTabSize(TAB_SIZE);
- aceOutputEditor.session.setUseSoftTabs(true);
+ aceOutputEditor = initializeAceEditor("ace-output", 'ace/mode/text', true, defaultMapping);
// Initialize metadata editor
- aceMetadataEditor = ace.edit("ace-metadata");
- aceMetadataEditor.setTheme("ace/theme/github");
- aceMetadataEditor.session.setMode("ace/mode/json"); // Use JSON mode for metadata
- aceMetadataEditor.setReadOnly(true); // Make metadata read-only
- aceMetadataEditor.session.setTabSize(TAB_SIZE);
- aceMetadataEditor.session.setUseSoftTabs(true);
+ aceOutputMetadataEditor = initializeAceEditor("ace-metadata", 'ace/mode/text', true, defaultMetaOutput);
choices = new Choices(document.getElementById("sample-dropdown"), {
searchEnabled: true,
@@ -130,16 +144,20 @@ document.addEventListener("DOMContentLoaded", () => {
document.getElementById("clear").addEventListener("click", () => {
aceInputEditor.setValue("");
aceMappingEditor.setValue("");
+ aceInputMetadataEditor.setValue("")
aceOutputEditor.setValue(defaultOutput, 1);
- aceMetadataEditor.setValue(defaultMeta, 1);
+ aceOutputMetadataEditor.setValue(defaultMetaOutput, 1);
saveTosessionStorage();
});
// Handle prettify button
document.getElementById("prettify").addEventListener("click", () => {
try {
- const formatted = JSON.stringify(JSON.parse(aceInputEditor.getValue()), null, 2);
- aceInputEditor.setValue(formatted, 1);
+ const formattedInput = JSON.stringify(JSON.parse(aceInputEditor.getValue()), null, 2);
+ const formattedInputMetadata = JSON.stringify(JSON.parse(aceInputMetadataEditor.getValue()), null, 2);
+ aceInputEditor.setValue(formattedInput, 1);
+ saveTosessionStorage();
+ aceInputMetadataEditor.setValue(formattedInputMetadata, 1);
saveTosessionStorage();
} catch (error) {
aceOutputEditor.setValue("Error: Invalid JSON input", 1);
@@ -156,13 +174,14 @@ document.addEventListener("DOMContentLoaded", () => {
if (sample) {
aceInputEditor.setValue(sample.input, 1);
aceMappingEditor.setValue(sample.mapping, 1);
+ aceInputMetadataEditor.setValue(sample.metadata || defaultMetaInput, 1);
execute();
saveTosessionStorage();
}
});
// Save content to sessionStorage and execute on changes
- [aceInputEditor, aceMappingEditor].forEach((editor) => {
+ [aceInputEditor, aceInputMetadataEditor, aceMappingEditor].forEach((editor) => {
editor.on("change", saveTosessionStorage);
editor.on("change", execute);
});
@@ -214,6 +233,10 @@ function getMapping() {
return aceMappingEditor ? aceMappingEditor.getValue() : "";
}
+function getInputMetadata() {
+ return aceInputMetadataEditor ? aceInputMetadataEditor.getValue() : "";
+}
+
function isValidJSON(str) {
try {
JSON.parse(str);
@@ -228,10 +251,10 @@ function execute() {
if (!metadataDetails) metadataDetails = document.getElementById("metadata-details");
aceOutputEditor.setValue("");
- aceMetadataEditor.setValue("");
+ aceOutputMetadataEditor.setValue("");
try {
- const result = blobl(getMapping(), getInput());
+ const result = blobl(getMapping(), getInput(), getInputMetadata());
if (isValidJSON(result)) {
const parsedResult = JSON.parse(result);
@@ -246,27 +269,27 @@ function execute() {
// Display metadata
if (Object.keys(metadata).length > 0) {
- aceMetadataEditor.session.setMode("ace/mode/json");
- aceMetadataEditor.setValue(JSON.stringify(metadata, null, 2), 1);
+ aceOutputMetadataEditor.session.setMode("ace/mode/json");
+ aceOutputMetadataEditor.setValue(JSON.stringify(metadata, null, 2), 1);
} else {
- aceMetadataEditor.session.setMode("ace/mode/text");
- aceMetadataEditor.setValue("{}", 1);
+ aceOutputMetadataEditor.session.setMode("ace/mode/text");
+ aceOutputMetadataEditor.setValue("{}", 1);
}
} else {
// If the result is not JSON, handle it as raw text
aceOutputEditor.session.setMode("ace/mode/text");
aceOutputEditor.setValue(result, 1);
- aceMetadataEditor.session.setMode("ace/mode/text");
- aceMetadataEditor.setValue("No metadata available", 1);
+ aceOutputMetadataEditor.session.setMode("ace/mode/text");
+ aceOutputMetadataEditor.setValue("No metadata available", 1);
}
} catch (error) {
// Handle general errors
aceOutputEditor.session.setMode("ace/mode/text");
aceOutputEditor.setValue("Error: " + error.message, 1);
- aceMetadataEditor.session.setMode("ace/mode/text");
- aceMetadataEditor.setValue("Error: " + error.message, 1);
+ aceOutputMetadataEditor.session.setMode("ace/mode/text");
+ aceOutputMetadataEditor.setValue("Error: " + error.message, 1);
}
}
@@ -274,12 +297,14 @@ function execute() {
function saveTosessionStorage() {
sessionStorage.setItem(sessionStorageKeys.input, aceInputEditor.getValue());
sessionStorage.setItem(sessionStorageKeys.mapping, aceMappingEditor.getValue());
+ sessionStorage.setItem(sessionStorageKeys.metadata, aceInputMetadataEditor.getValue());
}
// Restore editor content from sessionStorage
function restoreFromStorage() {
const savedInput = sessionStorage.getItem(sessionStorageKeys.input);
const savedMapping = sessionStorage.getItem(sessionStorageKeys.mapping);
+ const savedMeta = sessionStorage.getItem(sessionStorageKeys.metadata);
const dropdownElement = document.getElementById("sample-dropdown");
const samples = {{{page.attributes.bloblang-samples}}};
@@ -291,16 +316,19 @@ function restoreFromStorage() {
if (defaultSample) {
aceInputEditor.setValue(defaultSample.input, 1);
aceMappingEditor.setValue(defaultSample.mapping, 1);
+ defaultSample.metadata ? aceInputMetadataEditor.setValue(defaultSample.metadata, 1) : aceInputMetadataEditor.setValue(defaultMetaInput, 1)
choices.setChoiceByValue(defaultSample.title)
} else {
aceInputEditor.setValue(defaultInput, 1);
aceMappingEditor.setValue(defaultMapping, 1);
+ aceInputMetadataEditor.setValue(defaultMetaInput, 1);
choices.setChoiceByValue("");
}
} else {
// Restore values from storage
aceInputEditor.setValue(savedInput || defaultInput, 1);
aceMappingEditor.setValue(savedMapping || defaultMapping, 1);
+ aceInputMetadataEditor.setValue(savedMeta || defaultMetaInput, 1);
// Find and select the corresponding dropdown option
const matchingSample = Object.values(samples).find(
@@ -315,7 +343,7 @@ function restoreFromStorage() {
}
aceOutputEditor.setValue(defaultOutput, 1);
- aceMetadataEditor.setValue(defaultMeta, 1);
+ aceOutputMetadataEditor.setValue(defaultMetaOutput, 1);
}
diff --git a/src/partials/head-scripts.hbs b/src/partials/head-scripts.hbs
index 42a2efd2..799369a1 100644
--- a/src/partials/head-scripts.hbs
+++ b/src/partials/head-scripts.hbs
@@ -51,7 +51,7 @@ data-modal-disclaimer="This is a custom LLM for Redpanda with access to all deve
{{/if}}
-{{#if (eq page.attributes.role 'bloblang-playground')}}
+{{#if (or (eq page.attributes.role 'bloblang-playground')(eq page.attributes.role 'bloblang-snippets'))}}