dragble-export — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited dragble-export (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Export content from the Dragble editor in 6 formats. Save, load, validate, and manipulate designs programmatically.
The Dragble editor supports 6 export formats split into two tiers:
| Tier | Formats | Rendering | Plan |
|---|---|---|---|
| Client-side | HTML, JSON, Plain Text | Browser-based | Free |
| Server-side | Image, PDF, ZIP | Chromium headless | Paid |
Critical rule: Always save BOTH design JSON and HTML together. Design JSON is the editable source of truth; HTML is the rendered output. If you only save HTML, the design cannot be reloaded for editing.
All methods return Promises. There are no callback overloads.
Exports the current design as rendered HTML.
const html: string = await editor.exportHtml({
title: "My Campaign", // optional: sets <title> in the HTML
minify: true // optional: minifies the output HTML
});Signature:
exportHtml(options?: { title?: string; minify?: boolean }): Promise<string>Exports the current design as a JSON object representing the full design tree.
const designJson: DesignJson = await editor.exportJson();
// Store the JSON for later reloading
await fetch("/api/designs/123", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(designJson),
});Signature:
exportJson(): Promise<DesignJson>Exports the visible text content of the design, stripped of all HTML tags and formatting.
const text: string = await editor.exportPlainText();
console.log(text);
// "Hello World\nClick here to visit our site\n..."Signature:
exportPlainText(): Promise<string>Renders the design as an image via server-side Chromium. Requires a paid plan. Does NOT work in popup mode.
const imageData: ExportImageData = await editor.exportImage({
format: "png", // "png" | "jpeg" | "webp"
width: 600, // pixel width of the output
height: 0, // 0 = auto-height based on content
quality: 90, // 1-100, applies to jpeg/webp
deviceScaleFactor: 2, // retina scaling (1 | 2 | 3)
saveToStorage: true, // upload result to project storage
mergeTags: { // resolve merge tags before rendering
first_name: "Jane",
company: "Acme Inc."
}
});
// imageData.url - public URL (if saveToStorage: true)
// imageData.data - base64-encoded image dataSignature:
exportImage(options?: {
format?: "png" | "jpeg" | "webp";
width?: number;
height?: number;
quality?: number;
deviceScaleFactor?: number;
saveToStorage?: boolean;
mergeTags?: Record<string, string>;
}): Promise<ExportImageData>Return type:
interface ExportImageData {
url?: string; // present when saveToStorage is true
data: string; // base64-encoded image
}Renders the design as a PDF via server-side Chromium. Requires a paid plan. Does NOT work in popup mode.
const pdfData: ExportPdfData = await editor.exportPdf({
pageSize: "A4", // "A4" | "Letter" | "Legal" | custom
orientation: "portrait", // "portrait" | "landscape"
printBackground: true, // include background colors/images
margin: { // page margins
top: "10mm",
right: "10mm",
bottom: "10mm",
left: "10mm"
},
width: 600, // content width in pixels
saveToStorage: true, // upload result to project storage
mergeTags: {
first_name: "Jane"
}
});
// pdfData.url - public URL (if saveToStorage: true)
// pdfData.data - base64-encoded PDFSignature:
exportPdf(options?: {
pageSize?: string;
orientation?: "portrait" | "landscape";
printBackground?: boolean;
margin?: { top?: string; right?: string; bottom?: string; left?: string };
width?: number;
saveToStorage?: boolean;
mergeTags?: Record<string, string>;
}): Promise<ExportPdfData>Return type:
interface ExportPdfData {
url?: string; // present when saveToStorage is true
data: string; // base64-encoded PDF
}Bundles the design as a ZIP archive containing the HTML file and all referenced images. Requires a paid plan. Does NOT work in popup mode.
const zipData: ExportZipData = await editor.exportZip({
imageFolder: "images" // folder name for images inside the ZIP
});
// zipData.url - public URL (if saved to storage)
// zipData.data - base64-encoded ZIPSignature:
exportZip(options?: {
imageFolder?: string;
}): Promise<ExportZipData>Return type:
interface ExportZipData {
url?: string;
data: string; // base64-encoded ZIP
}Always save both the design JSON (for re-editing) and the rendered HTML (for sending/displaying):
async function saveDesign(editor: DragbleEditor, designId: string) {
// Export both in parallel
const [html, designJson] = await Promise.all([
editor.exportHtml({ minify: true }),
editor.exportJson(),
]);
// Persist to your backend
await fetch(`/api/designs/${designId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
html,
designJson,
updatedAt: new Date().toISOString(),
}),
});
}Listen for the editor ready event, then load the design:
editor.addEventListener("editor:ready", () => {
// Fetch the saved design JSON from your backend
fetch(`/api/designs/${designId}`)
.then((res) => res.json())
.then((data) => {
editor.loadDesign(data.designJson);
});
});Replaces the current design with the provided JSON. Does not validate the input.
editor.loadDesign(designJson);Signature:
loadDesign(design: DesignJson): voidReplaces the current design and resolves once the editor has finished rendering. Validates the design structure before loading.
try {
await editor.loadDesignAsync(designJson);
console.log("Design loaded and rendered");
} catch (err) {
console.error("Invalid design JSON:", err);
}Signature:
loadDesignAsync(design: DesignJson): Promise<void>Resets the editor to an empty design.
editor.loadBlank();Signature:
loadBlank(): voidReturns the current design JSON synchronously from the editor's in-memory state.
const currentDesign: DesignJson = editor.getDesign();Signature:
getDesign(): DesignJsonUse a debounced listener on the design:updated event to auto-save without flooding your backend. A 2-second debounce is recommended.
let saveTimeout: ReturnType<typeof setTimeout> | null = null;
editor.addEventListener("design:updated", () => {
// Clear any pending save
if (saveTimeout) {
clearTimeout(saveTimeout);
}
// Debounce: wait 2 seconds of inactivity before saving
saveTimeout = setTimeout(async () => {
try {
const [html, designJson] = await Promise.all([
editor.exportHtml({ minify: true }),
editor.exportJson(),
]);
await fetch(`/api/designs/${designId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ html, designJson }),
});
console.log("Auto-saved successfully");
} catch (err) {
console.error("Auto-save failed:", err);
}
}, 2000);
});The design JSON follows a strict tree hierarchy:
DesignJson
├── body: BodyValues
│ ├── backgroundColor
│ ├── contentWidth
│ ├── fontFamily
│ ├── preheaderText
│ └── ...global styles
├── rows: RowValues[]
│ ├── columns: ColumnValues[]
│ │ ├── contents: ContentData[]
│ │ │ ├── type: string
│ │ │ └── values: { ...per-type properties }
│ │ └── ...column styles
│ └── ...row styles
└── counters: Record<string, number>| Type | Description |
|---|---|
paragraph | Rich text block |
heading | Heading text (h1-h6) |
button | Call-to-action button |
image | Image with optional link |
divider | Horizontal rule / separator |
menu | Navigation link list |
html | Raw HTML block |
social | Social media icon links |
video | Embedded video player |
table | Data table |
timer | Countdown timer |
form | Input form |
spacer | Vertical spacing block |
The columns array length and relative weight values define the layout:
| columns | Layout |
|---|---|
[1] | 100% (full width) |
[1, 1] | 50% / 50% |
[1, 2] | 33.3% / 66.7% |
[2, 1] | 66.7% / 33.3% |
[1, 1, 1] | 33.3% / 33.3% / 33.3% |
[1, 2, 1] | 25% / 50% / 25% |
[1, 1, 1, 1] | 25% / 25% / 25% / 25% |
{
"body": {
"rows": [
{
"columns": [
{
"contents": [
{
"type": "paragraph",
"values": {
"text": "<p>Hello World</p>"
}
}
]
}
],
"values": {}
}
],
"values": {
"backgroundColor": "#ffffff",
"contentWidth": "600px",
"fontFamily": {
"label": "Arial",
"value": "arial,helvetica,sans-serif"
}
}
},
"counters": {}
}Registers a global design validator that runs before export operations.
editor.setValidator((design: DesignJson) => {
const errors: string[] = [];
if (!design.body.values.preheaderText) {
errors.push("Preheader text is required");
}
return { valid: errors.length === 0, errors };
});Signature:
setValidator(
validator: (design: DesignJson) => { valid: boolean; errors: string[] }
): voidRegisters a validator for a specific content type (tool). Runs when that tool's content is edited.
editor.setToolValidator("image", (values) => {
const errors: string[] = [];
if (!values.src || !values.alt) {
errors.push("Images must have alt text");
}
return { valid: errors.length === 0, errors };
});Signature:
setToolValidator(
toolName: string,
validator: (values: Record<string, unknown>) => { valid: boolean; errors: string[] }
): voidRemoves all registered validators (both global and tool-specific).
editor.clearValidators();Signature:
clearValidators(): voidRuns all registered validators against the current design and returns the results.
const result: AuditResult = await editor.audit();
if (result.status === "fail") {
console.error("Audit failed:", result.errors);
// result.errors: Array<{ tool?: string; message: string }>
}Signature:
audit(options?: { silent?: boolean }): Promise<AuditResult>Return type:
interface AuditResult {
status: "pass" | "fail";
errors: Array<{ tool?: string; message: string }>;
}Validates a design JSON structure without loading it into the editor.
const result = await editor.validateTemplate(designJson);
if (!result.valid) {
console.error("Template validation errors:", result.errors);
}Signature:
validateTemplate(
design: DesignJson
): Promise<{ valid: boolean; errors: string[] }>Validates values for a specific content type without loading them.
const result = await editor.validateTool("button", {
text: "Click Me",
href: ""
});
// result.valid === false, result.errors === ["Button must have a link URL"]Signature:
validateTool(
toolName: string,
values: Record<string, unknown>
): Promise<{ valid: boolean; errors: string[] }>Selects an element in the editor by its unique ID, opening its property panel.
editor.selectElement("u_content_heading_1");Signature:
selectElement(elementId: string): voidHighlights an element in the editor with a visual indicator. Pass null to clear the highlight.
// Highlight an element
editor.highlightElement("u_content_image_3");
// Clear highlight
editor.highlightElement(null);Signature:
highlightElement(elementId: string | null): voidScrolls the editor viewport to bring the specified element into view.
editor.scrollToElement("u_row_5");Signature:
scrollToElement(elementId: string): voidExecutes an editor command on the currently selected element.
editor.execCommand("bold");
editor.execCommand("fontSize", "18px");
editor.execCommand("foreColor", "#ff0000");Signature:
execCommand(command: string, value?: string): voidWrong:
const html = await editor.exportHtml();
await saveToBackend({ html });
// Design cannot be reloaded for editing!Correct:
const [html, designJson] = await Promise.all([
editor.exportHtml(),
editor.exportJson(),
]);
await saveToBackend({ html, designJson });These methods require a paid plan and server-side Chromium rendering. They will reject with an error on free plans:
try {
const image = await editor.exportImage({ format: "png" });
} catch (err) {
// "Export to image requires a paid plan"
}Server-side export methods are not available when the editor is opened in popup mode. Use them only in the embedded (iframe) editor mode.
Wrong:
const editor = new DragbleEditor("#container", config);
editor.loadDesign(designJson); // Editor not ready yet!Correct:
const editor = new DragbleEditor("#container", config);
editor.addEventListener("editor:ready", () => {
editor.loadDesign(designJson);
});Without debouncing, design:updated fires on every keystroke and property change. Always debounce with at least a 2-second delay (see the Auto-Save section above).
The design JSON must follow the exact tree hierarchy: body.rows[].columns[].contents[]. Missing any level will cause loadDesign to fail silently or produce a broken layout. Use loadDesignAsync to catch validation errors, or call validateTemplate before loading.
All export methods return Promises that can reject. Always wrap them in try/catch:
try {
const html = await editor.exportHtml({ minify: true });
} catch (err) {
console.error("Export failed:", err);
}~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.