Skip to content

Instantly share code, notes, and snippets.

@duttonw
Last active October 29, 2024 23:13
Show Gist options
  • Save duttonw/a2d6223b6d6ca2b41295b75c811f64d0 to your computer and use it in GitHub Desktop.
Save duttonw/a2d6223b6d6ca2b41295b75c811f64d0 to your computer and use it in GitHub Desktop.
Embed Svg's for Handlebars (with build embed as well as dynamic on template compile) for us in (Vite or esbuild) build systems.
import Handlebars from "handlebars";
/**
* Embeds an SVG file into the template
*
* Do note: rendering when inside handlebars will not be relative to a imported template file.
*
* It has two modes, browser mode and node mode.
* When in browser mode, it will place a random id to be filled in when the file is successfully collected
*
*
* @param filePath
* @param options
* @returns {Handlebars.SafeString|string}
*/
export default function( filePath, options) {
//console.log(filePath)
if (typeof window === 'undefined') {
// Node.js environment
const fs = require('fs');
const path = require('path');
try {
const fullPath = path.resolve(filePath);
const svgContent = fs.readFileSync(fullPath, 'utf8');
return new Handlebars.SafeString(svgContent);
} catch (error) {
console.error(`Error reading SVG file: ${filePath}`, error);
throw error;
}
} else {
// Browser environment
// Using a placeholder while we fetch the content later
const id = `svg-${Math.random().toString(36).substr(2, 9)}`;
fetch(filePath)
.then(response => {
if (!response.ok) {
throw new Error(`Failed to fetch SVG: ${response.statusText}, ${filePath}`);
}
return response.text();
})
.then(svgContent => {
// Insert the SVG content into the DOM after fetching
const element = document.getElementById(id);
if (element) {
element.innerHTML = svgContent;
}
})
.catch(error => {
console.error(`Error fetching SVG file: ${filePath}`, error);
});
// Return a placeholder div with a unique ID
return new Handlebars.SafeString(`<div id="${id}">Loading SVG...</div>`);
}
};
import fs from 'fs';
import path from 'path';
/**
* This file contains a plugin for Vite and esbuild that embeds SVG files into Handlebars templates.
*
* The format it is looking for is {{ embedSvgs "./image.svg" }}
* The regex is:
* esbuild: {{\s*embedSvg\s*"([^"]+)"\s*}}
* vite : {{\s*embedSvg\s*\\"([^"]+)\\"\s*}} <-- Vite auto escapes double quotes
*
* The plugin reads the SVG file specified in the embedSvg tag and replaces the tag with the content of the SVG file.
*
* The file path is relative to the Handlebars file that contains the embedSvg tag.
*
* There is also the [embedSvg](./src/helpers/Handlebars/embedSvg.js) helper function.
* Do note: rendering when inside handlebars can't relative reference the imported template file.
*/
/**
* Escapes backslashes, single quotes, and double quotes in a string for JavaScript.
* @param content
* @returns {*}
*/
function escapeForJavaScript(content) {
// Escape backslashes, single quotes, and double quotes
return content
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/"/g, '\\"')
.replace(/\r?\n/g, '\\n'); // Escape newlines for JavaScript strings
}
export function viteHandlebarsEmbedSvgPlugin() {
return {
name: 'vite-embed-svg',
enforce: 'pre', // Ensure this plugin runs before other transforms
transform(code, id) {
if (!id.endsWith('.js') && !id.endsWith('.hbs?raw') && !id.endsWith('.hbs')) {
return null; // Only process .hbs files
}
// if (id.endsWith("/header/html/component.hbs?raw" || id.endsWith("handlebars.partials.js"))) {
// console.error(id)
// //console.error(code)
// }
let changed = false;
// Regex pattern to match {{embedSvg "file.svg"}}
const svgEmbedPattern = /{{\s*embedSvg\s*\\"([^"]+)\\"\s*}}/g;
// Replace {{embedSvg "file.svg"}} with the content of the SVG
const transformedCode = code.replace(svgEmbedPattern, (match, filePath) => {
console.error("found embedSvg file:" + path.resolve(path.dirname(id), filePath) + " in file:" + id)
changed = true
try {
const svgPath = path.resolve(path.dirname(id), filePath);
const svgContent = fs.readFileSync(svgPath, 'utf8');
return escapeForJavaScript(svgContent);
} catch (error) {
console.error(`Error embedding SVG for ${filePath}:`, error);
return match; // Leave the original tag if there's an error
}
});
if (changed) {
//console.error("returning changed")
// Return the transformed code
return transformedCode
} else {
// no change
return null; // Only process .hbs files
}
},
};
}
export function esBuildHandlebarsEmbedSvgPlugin() {
return {
name: 'embed-svg',
setup(build) {
build.onLoad({ filter: /\.hbs$/ }, async (args) => {
let contents = await fs.promises.readFile(args.path, 'utf8');
if (args.path.endsWith("/header/html/component.hbs" || args.path.endsWith("handlebars.partials.js"))) {
console.error(args.path)
//console.error(contents)
}
// Regex pattern to match {{embedSvg "file.svg"}}
const svgEmbedPattern = /{{\s*embedSvg\s*"([^"]+)"\s*}}/g;
// Replace {{embedSvg "file.svg"}} with the content of the SVG
contents = contents.replace(svgEmbedPattern, (match, filePath) => {
//console.error(match);
//console.error(filePath);
try {
const dirPath = path.dirname(args.path)
const svgPath = path.resolve(path.dirname(args.path), filePath);
//console.error(dirPath );
//console.error(svgPath );
const svgContent = fs.readFileSync(svgPath, 'utf8');
return svgContent;
} catch (error) {
console.error(`Error embedding SVG: ${filePath} in file: ${args.path}`);
throw error;
//return match; // Leave the original tag if there's an error
}
});
// Return the modified contents to esbuild
return {
contents,
loader: 'text',
};
});
},
};
}
//.storybook/main.js
const { mergeConfig } = require('vite');
const customViteConfig = require('../vite.config.js'); // Adjust the path as needed
/** @type { import('@storybook/html-vite').StorybookConfig } */
const config = {
...all other config goes here
framework: {
//Build the storybook with html-vite rendered - faster than webpack
//https://www.npmjs.com/package/@storybook/html-vite
name: "@storybook/html-vite",
options: {},
},
// https://storybook.js.org/docs/api/main-config-vite-final
// Use the Vite configuration from the main project (yes this is a esbuild project but storybook uses vite)
async viteFinal(config) {
// Merge custom Vite configuration
return mergeConfig(config, customViteConfig);
},
};
export default config;
import embedSvg from "./Handlebars/embedSvg.js";
export default function handlebarsHelpersRollup(handlebars) {
handlebars.registerHelper("embedSvg", embedSvg);
}
import { defineConfig } from 'vite';
import { viteHandlebarsEmbedSvgPlugin } from './handlebarsEmbedSvgPlugin.js'
export default defineConfig({
root: './dist',
plugins: [
{
name: "html-transform",
transform(src, id) {
if (id.endsWith(".mustache") || id.endsWith(".html") || id.endsWith(".hbs")) {
// Transform your HTML files here (src is the file content as a string)
return src;
}
},
},
viteHandlebarsEmbedSvgPlugin()
],
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment