Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DOC-916 Embeddable Blobl playground editor #91

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions local-antora-playbook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ asciidoc:
- '@asciidoctor/tabs'
- './macros/glossary'
- './macros/config-ref'
- './macros/bloblang-snippet'
- './macros/helm-ref'
- './macros/rp-connect-components'
- './asciidoc-extensions/add-line-numbers-highlights'
Expand Down
201 changes: 201 additions & 0 deletions macros/bloblang-snippet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
module.exports.register = function (registry) {
registry.block(function () {
var self = this
self.named('bloblang_snippet')
self.onContext(['paragraph','open','literal'])
self.process(function (parent, reader, attrs) {
const input = attrs['input'] || '{}';
const metadata = attrs['metadata'] || '{}';
const wasmUrl = attrs['wasm_url'] || '/blobl.wasm';
const instanceId = `bloblang-${Math.random().toString(36).slice(2, 11)}`;

// Read block content (e.g., mapping)
const mapping = reader.getLines().join('\n');

const html = `
<div id="${instanceId}" class="bloblang-snippet">
<div class="row" id="${instanceId}-input-row">
<div class="box" id="${instanceId}-input-box" style="display: none;">
<b>Input</b>
<div id="${instanceId}-input" class="ace-editor"></div>
</div>
<div class="box" id="${instanceId}-metadata-box" style="display: none;">
<b>Input metadata</b>
<div id="${instanceId}-metadata" class="ace-editor"></div>
</div>
</div>
<div class="row">
<div class="box full-width">
<b>Bloblang mapping</b>
<div id="${instanceId}-mapping" class="ace-editor"></div>
</div>
</div>
<div class="row">
<div class="box">
<b>Output</b>
<div id="${instanceId}-output" class="ace-editor">"Output will appear here..."</div>
</div>
<div class="box" id="${instanceId}-output-metadata-box" style="display: none;">
<b>Output metadata</b>
<div id="${instanceId}-output-metadata" class="ace-editor">"Metadata will appear here..."</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const wasmUrl = "${wasmUrl}";
const inputEditorId = "${instanceId}-input";
const mappingEditorId = "${instanceId}-mapping";
const metadataEditorId = "${instanceId}-metadata";
const outputEditorId = "${instanceId}-output";
const outputMetadataEditorId = "${instanceId}-output-metadata";

const metadataBoxId = "${instanceId}-metadata-box";
const inputBoxId = "${instanceId}-input-box";
const outputMetadataBoxId = "${instanceId}-output-metadata-box";

let inputEditor, mappingEditor, metadataEditor, outputEditor, outputMetadataEditor, wasmInstance;

// 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.renderer.setOption('showGutter', false);
editor.setOptions({
minLines: 1, // Minimum height
maxLines: 20,
});
editor.setStyle('bloblang-editor')
editor.container.style.lineHeight = 1.7
editor.renderer.updateFontSize()
editor.renderer.setScrollMargin(14, 14, 0, 0)
return editor;
}

// Check if argument is empty
function isEmpty(data) {
try {
const parsed = JSON.parse(data);
return Object.keys(parsed).length === 0;
} catch (e) {
return true; // Treat invalid data as empty
}
}

inputEditor = initializeAceEditor(inputEditorId, 'ace/mode/json', false, ${input ? JSON.stringify(input) : '{}'});
mappingEditor = initializeAceEditor(mappingEditorId, 'ace/mode/coffee', false, \`${mapping}\`);
outputEditor = initializeAceEditor(outputEditorId, 'ace/mode/text', true, '"Output will appear here..."');
metadataEditor = initializeAceEditor(metadataEditorId, 'ace/mode/json', false, ${metadata ? JSON.stringify(metadata) : '{}'});
outputMetadataEditor = initializeAceEditor(outputMetadataEditorId, 'ace/mode/json', true, '"Metadata will appear here..."');

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
}
}

// Load WASM
async function loadWasm() {
const go = new Go();
const result = await WebAssembly.instantiateStreaming(fetch(wasmUrl), go.importObject);
go.run(result.instance);
wasmInstance = window.blobl;
executeMapping();
}

function isValidJSON(str) {
try {
JSON.parse(str);
return true;
} catch (e) {
return false;
}
}

// Execute Mapping
async function executeMapping() {
try {
const input = inputEditor.getValue();
const mapping = mappingEditor.getValue();
const metadata = metadataEditor.getValue();

if (!isEmpty(metadata)) {
document.getElementById(metadataBoxId).style.display = 'block';
}
if (!isEmpty(input)) {
document.getElementById(inputBoxId).style.display = 'block';
}

if (!input || !mapping) {
outputEditor.setValue('"Input and mapping are required."', 1);
return;
}

// Ensure valid JSON for input and metadata
if (!isValidJSON(input)) {
outputEditor.setValue('"Error: Invalid JSON input."', 1);
return;
}
if (metadata && !isValidJSON(metadata)) {
outputEditor.setValue('"Error: Invalid JSON metadata."', 1);
return;
}

// Call the WASM function
const result = wasmInstance(mapping, input, metadata || '{}');

if (isValidJSON(result)) {
const parsedResult = JSON.parse(result);

// Separate message and metadata
const message = parsedResult.msg || parsedResult;
const outputMetadata = parsedResult.meta || {};

// Display output metadata box if metadata exists
if (Object.keys(outputMetadata).length > 0) {
document.getElementById(outputMetadataBoxId).style.display = 'block';
outputMetadataEditor.setValue(JSON.stringify(outputMetadata, null, 2), 1);
} else {
document.getElementById(outputMetadataBoxId).style.display = 'none';
outputMetadataEditor.setValue('"No metadata available."', 1);
}

// Display message output
outputEditor.session.setMode("ace/mode/json");
outputEditor.setValue(JSON.stringify(message, null, 2), 1);
} else {
// If the result isn't valid JSON, treat it as plain text
outputEditor.session.setMode("ace/mode/text");
outputEditor.setValue(result, 1);
}
} catch (error) {
outputEditor.session.setMode("ace/mode/text");
outputEditor.setValue('"Error: ' + error.message + '"', 1);
}
}

// Attach change listeners to auto-execute
inputEditor.session.on('change', executeMapping);
mappingEditor.session.on('change', executeMapping);
metadataEditor.session.on('change', executeMapping);
document.getElementById(inputEditorId).style.fontFamily='"IBM Plex Mono", "Courier Prime", courier, monospace';
document.getElementById(mappingEditorId).style.fontFamily='"IBM Plex Mono", "Courier Prime", courier, monospace';
document.getElementById(metadataEditorId).style.fontFamily='"IBM Plex Mono", "Courier Prime", courier, monospace';
document.getElementById(outputMetadataEditorId).style.fontFamily='"IBM Plex Mono", "Courier Prime", courier, monospace';

// Load WASM and Handle Errors
loadWasm().catch(err => {
outputEditor.setValue('"Error loading WASM file: ' + err.message + '"', 1);
});
});
</script>
`;
return this.createBlock(parent, 'pass', html);
});
});
};
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@redpanda-data/docs-extensions-and-macros",
"version": "4.1.0",
"version": "4.2.0",
"description": "Antora extensions and macros developed for Redpanda documentation.",
"keywords": [
"antora",
Expand Down Expand Up @@ -50,7 +50,8 @@
"./macros/glossary": "./macros/glossary.js",
"./macros/rp-connect-components": "./macros/rp-connect-components.js",
"./macros/config-ref": "./macros/config-ref.js",
"./macros/helm-ref": "./macros/helm-ref.js"
"./macros/helm-ref": "./macros/helm-ref.js",
"./macros/bloblang-snippet": "./macros/bloblang-snippet.js"
},
"files": [
"extensions",
Expand Down