sionzee/rollup-plugin-inline-svg

Support for inlining SVG's in html files

lllopo opened this issue · 3 comments

Useful plugin, but I'm missing one critical feature - processing of HTML files and replacing all (or tagged) SVG;s in the index.html for example with their svg source XML. For example :
<img svg-inline src="/img/svg/apple.svg" style="font-size: 1.1em; margin-top: -2px;" class="current-color svg-inline--fa fa-phone fa-w-16 me-2"/>
should be replaced with it's source keeping the image attributes, like this :
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 500 500" enable-background="new 0 0 500 500" xml:space="preserve" style="font-size: 1.1em; margin-top: -2px;" class="current-color svg-inline--fa fa-phone fa-w-16 me-2"> <path ..... /> </svg>

Note the 'svg-inline' attribute which can serve for a (configurable) tag if the SVG should be inlined or not.

Hi @lllopo, thank you for the feature request. The purpose of this plugin was to defacto copy the svg's content into a file where it is referenced. Meanwhile, you ask to parse HTML files and replace the match with a different value.

That is a fundamental change to the concept of this plugin. Therefore the solution for your issue would be to use html-to-ast and traverse the HTML directly.

// Rollup plugin
export default function CustomSvgInlinerPlugin() {
  return {
    name: 'CustomSvgInlinerPlugin',

    transform(code: string, id: string) {
      if(!id.endsWith('.html')) return
      return {code: transformHtml(html), map: {mappings: ''}};
    },
  };
}

// Implementation
import fs from 'node:fs'
import { parse, stringify } from 'html-to-ast';

export type AstNode = ReturnType<typeof parse>[number]

const traverse = (node: AstNode, callbackFn: (node: AstNode) => void): void {
    if(callbackFn(node) && node.children) node.children.forEach(child => this.traverse(child, callbackFn))
}

export const transformHtml = (html: string): string => {
  const asts = parse(code.trim()) as AstNode[]
  
  // Let's hope you have one main root. Otherwise, you will need to wrap this into asts.forEach
  const ast = asts.pop()!
  
  traverse(ast, (node: AstNode) => {
    if(node.name === 'img' && node.attrs['svg-inline'] !== undefined) {
      const svgFile = fs.readFileSync(node.attrs.src) // You will probably need to do something like path.resolve(__dirname, node.attrs.src)  
      const svgAst = parse(svgFile)[0] // Again, hope it has just 1 root element <svg>...</svg>
      
      // Remap the img to svg
      node.children = svgAst.children;
      node.name = 'svg'
      node.attrs = {...svgAst.attrs, ...node.attrs}
      return false; // we don't want to iterate the children because we just replaced them
    }
    return true
  })

  return stringify([ast] as any[])
}

I was not testing it, but the principle is straightforward, and I hope it will help you build your desired feature. To me, for these specific features is always better to write a custom solution. You will have complete control over the feature and can do whatever you want.

Please do not be offended when I close this issue, I will be happy to discuss it more if you need any help. I wish you happy coding 💻 ⌨️ 📝

@sionzee Thanks for you quick and kind reply. It is, of course, your choice to decide the direction where you go with the plugin. Mine was just as suggestion, based on the need I have, so it is perfectly fine. Actually, after doing a bit more digging (and found nothing suitable), I took the road you suggested and wrote my own plugin. It is using 'node-html-parser', though as a more popular library. Although it behaves a bit oddly, it does the job and I have it up and running already. Again, thanks and keep up the good work.