├── .editorconfig ├── .gitignore ├── CHANGELOG.md ├── README.md ├── babel.config.js ├── package.json ├── src └── loader.js ├── test ├── compiler.js ├── examples │ ├── auto-svgo.html │ ├── auto-svgo.js.gen │ ├── basic-no-inline.html │ ├── basic-no-inline.js.gen │ ├── basic.html │ ├── basic.js.gen │ ├── custom-strict-selector.html │ ├── custom-strict-selector.js.gen │ ├── eslint.json │ ├── images │ │ ├── basic.svg │ │ ├── complex.svg │ │ ├── test.jpg │ │ └── test.mml │ ├── img-multiline.html │ ├── img-multiline.js.gen │ ├── img.html │ ├── img.js.gen │ ├── math-ml.html │ ├── math-ml.js.gen │ ├── non-svg-img.html │ ├── non-svg-img.js.gen │ ├── svg-with-attributes.html │ └── svg-with-attributes.js.gen └── loader.spec.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # OS generated files # 6 | .DS_Store 7 | ehthumbs.db 8 | Icon? 9 | Thumbs.db 10 | 11 | # Node Files # 12 | /node_modules 13 | npm-debug.log 14 | 15 | # Typing # 16 | /src/typings/tsd/ 17 | /typings/ 18 | /tsd_typings/ 19 | 20 | # Dist # 21 | /dist 22 | 23 | # IDE # 24 | .idea/ 25 | *.swp 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Breaking changes 2 | 3 | ## v0.2.0 4 | 5 | By default, strict to `svg[markup-inline], img[markup-inline], math[markup-inline], svg[data-markup-inline], img[data-markup-inline], math[data-markup-inline]`. 6 | 7 | All elements that do not match this selector are ignored. 8 | 9 | ## v4.0.0 10 | 11 | Upgrade to Webpack 4.x SDK & Compatible with Webpack 5.x 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What's this? 2 | 3 | This is a webpack loader. It can inline SVG or MathML file to HTML, so that you can apply css to embedded svg. 4 | 5 | ## Breaking changes 6 | 7 | ### v0.2 8 | 9 | In previous versions, the `strict` option defaults to '', which means that it will handle all svg pictures. But it easily leads to unexpected results, and now we set it to `[markup-inline]`: `svg[markup-inline], img[markup-inline], math[markup-inline], svg[data-markup-inline], img[data-markup-inline], math[data-markup-inline]`. 10 | 11 | All elements that do not match these selectors are ignored. 12 | 13 | ## Example 14 | 15 | ### Configuration 16 | 17 | ```js 18 | const rules = [ 19 | { 20 | test: /\.html$/, 21 | use: 'markup-inline-loader', 22 | }, 23 | ]; 24 | ``` 25 | 26 | It will inline the svg file and return the inlined html (instead of js format) 27 | 28 | Or with `html-loader`: 29 | 30 | ```js 31 | const rules = [ 32 | { 33 | test: /\.html$/, 34 | use: [ 35 | 'html-loader', 36 | 'markup-inline-loader', 37 | ], 38 | }, 39 | ] 40 | ``` 41 | 42 | Or with `html-loader` and a SVGO configuration. By default `markup-inline-loader` only enables the removeTitle plugin. You can overwrite this default behavior with following example: 43 | 44 | ```js 45 | const rules = [ 46 | { 47 | test: /\.html$/, 48 | use: [ 49 | 'html-loader', 50 | { 51 | loader: 'markup-inline-loader', 52 | options: { 53 | svgo: { 54 | plugins: [ 55 | { 56 | removeTitle: true, 57 | }, 58 | { 59 | removeUselessStrokeAndFill: false, 60 | }, 61 | { 62 | removeUnknownsAndDefaults: false, 63 | }, 64 | ], 65 | }, 66 | }, 67 | }, 68 | ], 69 | }, 70 | ]; 71 | ``` 72 | 73 | By default, it's apply to: 74 | 75 | ```html 76 | 77 | ``` 78 | 79 | and 80 | 81 | ```html 82 | 83 | ``` 84 | 85 | but not apply to: 86 | 87 | ```html 88 | 89 | ``` 90 | 91 | We call the `[markup-inline]` and `[data-markup-inline]` as `strict`. 92 | 93 | We can also customize the `strict`. e.g. 94 | 95 | ``` 96 | const rules = [ 97 | { 98 | test: /\.html$/, 99 | use: [ 100 | 'html-loader', 101 | 'markup-inline-loader?strict=[markup-inline]', 102 | ], 103 | ]; 104 | ``` 105 | 106 | Note the strict value is a css selector, but currently we support attribute selector only. 107 | 108 | ### Original HTML 109 | 110 | ```html 111 | 112 | ``` 113 | 114 | ### Translated HTML 115 | 116 | ```svg 117 | 118 | 119 | 120 | ``` 121 | 122 | So we can apply css styles to `svg > path {}`. 123 | 124 | or 125 | 126 | ```svg 127 | 128 | 129 | 131 | 136 | 137 | 138 | 139 | 141 | 142 | 143 | ``` 144 | 145 | So we can apply css animations to `svg > .text`, for example: 146 | 147 | ```css 148 | @keyframes rotate { 149 | from { 150 | transform: rotateY(0deg); 151 | } 152 | to { 153 | transform: rotateY(-180deg); 154 | } 155 | } 156 | 157 | svg > .text { 158 | animation: 3s infinite rotate; 159 | transform-origin: center; 160 | } 161 | ``` 162 | 163 | ## Contributors 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | Thank you! 172 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markup-inline-loader", 3 | "version": "4.0.0", 4 | "description": "inline markups to HTML, such as SVG, MathML.", 5 | "main": "src/loader.js", 6 | "scripts": { 7 | "test": "jest" 8 | }, 9 | "jest": { 10 | "testEnvironment": "node" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/asnowwolf/markup-inline-loader.git" 15 | }, 16 | "keywords": [ 17 | "webpack", 18 | "loader", 19 | "svg", 20 | "mathml", 21 | "markup" 22 | ], 23 | "author": "Zhicheng Wang", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/asnowwolf/markup-inline-loader/issues" 27 | }, 28 | "homepage": "https://github.com/asnowwolf/markup-inline-loader#readme", 29 | "devDependencies": { 30 | "@babel/core": "^7.12.10", 31 | "@babel/preset-env": "^7.12.11", 32 | "babel-jest": "^26.6.3", 33 | "file-loader": "^6.2.0", 34 | "html-loader": "^1.3.2", 35 | "image-loader": "^0.0.1", 36 | "jest": "^26.6.3", 37 | "memfs": "^3.2.0", 38 | "webpack": "^4.0.0" 39 | }, 40 | "dependencies": { 41 | "loader-utils": "^2.0.0", 42 | "svgo": "^1.3.0" 43 | }, 44 | "peerDependencies": { 45 | "webpack": "^4.0.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/loader.js: -------------------------------------------------------------------------------- 1 | const PATTERN = /<(svg|img|math)\s+([^>]*?)src\s*=\s*"([^>]*?)"([^>]*?)\/?>/gi; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const SVGO = require('svgo'); 6 | const loaderUtils = require('loader-utils'); 7 | 8 | const SVGOConfiguration = { 9 | plugins: [ 10 | {removeTitle: true}, 11 | ], 12 | }; 13 | 14 | module.exports = async function loader(source) { 15 | this.cacheable && this.cacheable(); 16 | const callback = this.async(); 17 | const result = await replace(source, this); 18 | callback(null, result); 19 | }; 20 | 21 | async function replace(source, loader) { 22 | const options = loaderUtils.getOptions(loader); 23 | const svgo = new SVGO(options.svgo || SVGOConfiguration); 24 | const strict = (options.strict || '[markup-inline]').replace(/\[(data-)?([\w-]+)]/, '$2'); 25 | 26 | const result = []; 27 | const tokens = source.matchAll(PATTERN); 28 | let prevPos = 0; 29 | for (const token of tokens) { 30 | const [matched, tagName, preAttributes, fileName, postAttributes] = token; 31 | const {index} = token; 32 | const isSvgFile = path.extname(fileName).toLowerCase() === '.svg'; 33 | const isImg = tagName.toLowerCase() === 'img'; 34 | const meetStrict = new RegExp(`\\b(data-)?${strict}\\b`).test(preAttributes + ' ' + postAttributes); 35 | 36 | if (isImg && !isSvgFile || !meetStrict) { 37 | continue; 38 | } 39 | 40 | const filePath = loaderUtils.urlToRequest(path.join(loader.context, fileName), '/'); 41 | loader.addDependency(filePath); 42 | let fileContent = fs.readFileSync(filePath, 'utf8'); 43 | if (isSvgFile) { 44 | // It's callback, But it's sync call, So, we needn't use async loader 45 | fileContent = (await svgo.optimize(fileContent)).data; 46 | } 47 | if (index !== prevPos) { 48 | result.push(source.slice(prevPos, index)); 49 | } 50 | result.push(fileContent.replace(/^<(svg|math)/, '<$1 ' + preAttributes + postAttributes + ' ')); 51 | prevPos = index + matched.length; 52 | } 53 | result.push(source.slice(prevPos)); 54 | return result.join(''); 55 | } 56 | -------------------------------------------------------------------------------- /test/compiler.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import {createFsFromVolume, Volume} from 'memfs'; 4 | 5 | module.exports = (fixture, options = {}) => { 6 | const compiler = webpack({ 7 | context: __dirname, 8 | entry: `./${fixture}`, 9 | output: { 10 | path: path.resolve(__dirname), 11 | filename: 'bundle.js', 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.html$/, 17 | use: [ 18 | 'html-loader', 19 | { 20 | loader: path.resolve(__dirname, '../src/loader.js'), 21 | options, 22 | }, 23 | ], 24 | }, 25 | { 26 | test: /\.jpg$/, 27 | use: 'file-loader', 28 | }, 29 | { 30 | test: /\.svg$/, 31 | use: 'file-loader', 32 | }, 33 | ], 34 | }, 35 | }); 36 | 37 | compiler.outputFileSystem = createFsFromVolume(new Volume()); 38 | compiler.outputFileSystem.join = path.join.bind(path); 39 | 40 | return new Promise((resolve, reject) => { 41 | compiler.run((err, stats) => { 42 | if (err) reject(err); 43 | if (stats.hasErrors()) reject(stats.toJson().errors); 44 | 45 | resolve(stats); 46 | }); 47 | }); 48 | }; 49 | -------------------------------------------------------------------------------- /test/examples/auto-svgo.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/examples/auto-svgo.js.gen: -------------------------------------------------------------------------------- 1 | // Module 2 | var code = "Some Title "; 3 | // Exports 4 | module.exports = code; 5 | -------------------------------------------------------------------------------- /test/examples/basic-no-inline.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/examples/basic-no-inline.js.gen: -------------------------------------------------------------------------------- 1 | // Imports 2 | var ___HTML_LOADER_GET_SOURCE_FROM_IMPORT___ = require("../../node_modules/html-loader/dist/runtime/getUrl.js"); 3 | var ___HTML_LOADER_IMPORT_0___ = require("./images/basic.svg"); 4 | // Module 5 | var ___HTML_LOADER_REPLACEMENT_0___ = ___HTML_LOADER_GET_SOURCE_FROM_IMPORT___(___HTML_LOADER_IMPORT_0___); 6 | var code = " "; 7 | // Exports 8 | module.exports = code; 9 | -------------------------------------------------------------------------------- /test/examples/basic.html: -------------------------------------------------------------------------------- 1 | abc 2 | -------------------------------------------------------------------------------- /test/examples/basic.js.gen: -------------------------------------------------------------------------------- 1 | // Module 2 | var code = "abc "; 3 | // Exports 4 | module.exports = code; 5 | -------------------------------------------------------------------------------- /test/examples/custom-strict-selector.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /test/examples/custom-strict-selector.js.gen: -------------------------------------------------------------------------------- 1 | // Imports 2 | var ___HTML_LOADER_GET_SOURCE_FROM_IMPORT___ = require("../../node_modules/html-loader/dist/runtime/getUrl.js"); 3 | var ___HTML_LOADER_IMPORT_0___ = require("./images/basic.svg"); 4 | // Module 5 | var ___HTML_LOADER_REPLACEMENT_0___ = ___HTML_LOADER_GET_SOURCE_FROM_IMPORT___(___HTML_LOADER_IMPORT_0___); 6 | var code = " "; 7 | // Exports 8 | module.exports = code; 9 | -------------------------------------------------------------------------------- /test/examples/eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | } 4 | } 5 | -------------------------------------------------------------------------------- /test/examples/images/basic.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/examples/images/complex.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Some Title 5 | A complex svg for test 'svgo' 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/examples/images/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asnowwolf/markup-inline-loader/33267b90e564a022b66bff691ce016b69e35251d/test/examples/images/test.jpg -------------------------------------------------------------------------------- /test/examples/images/test.mml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/examples/img-multiline.html: -------------------------------------------------------------------------------- 1 |
Test
3 | -------------------------------------------------------------------------------- /test/examples/img-multiline.js.gen: -------------------------------------------------------------------------------- 1 | // Module 2 | var code = "
Test
"; 3 | // Exports 4 | module.exports = code; 5 | -------------------------------------------------------------------------------- /test/examples/img.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/examples/img.js.gen: -------------------------------------------------------------------------------- 1 | // Module 2 | var code = " "; 3 | // Exports 4 | module.exports = code; 5 | -------------------------------------------------------------------------------- /test/examples/math-ml.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/examples/math-ml.js.gen: -------------------------------------------------------------------------------- 1 | // Module 2 | var code = " "; 3 | // Exports 4 | module.exports = code; 5 | -------------------------------------------------------------------------------- /test/examples/non-svg-img.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/examples/non-svg-img.js.gen: -------------------------------------------------------------------------------- 1 | // Imports 2 | var ___HTML_LOADER_GET_SOURCE_FROM_IMPORT___ = require("../../node_modules/html-loader/dist/runtime/getUrl.js"); 3 | var ___HTML_LOADER_IMPORT_0___ = require("./images/test.jpg"); 4 | // Module 5 | var ___HTML_LOADER_REPLACEMENT_0___ = ___HTML_LOADER_GET_SOURCE_FROM_IMPORT___(___HTML_LOADER_IMPORT_0___); 6 | var code = " "; 7 | // Exports 8 | module.exports = code; 9 | -------------------------------------------------------------------------------- /test/examples/svg-with-attributes.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/examples/svg-with-attributes.js.gen: -------------------------------------------------------------------------------- 1 | // Module 2 | var code = " "; 3 | // Exports 4 | module.exports = code; 5 | -------------------------------------------------------------------------------- /test/loader.spec.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | /** 4 | * @jest-environment node 5 | */ 6 | const compiler = require('./compiler.js'); 7 | 8 | function readJs(filename) { 9 | return fs.readFileSync(path.join(__dirname, filename), 'utf8').trim(); 10 | } 11 | 12 | describe('markup inline loader', () => { 13 | it('should inline svg[src$=.svg]', async () => { 14 | const stats = await compiler('./examples/basic.html'); 15 | const output = stats.toJson({source: true}).modules[0].source; 16 | expect(output).toBe(readJs('./examples/basic.js.gen')); 17 | }); 18 | it('should inline img[src$=.svg]', async () => { 19 | const stats = await compiler('./examples/img.html'); 20 | const output = stats.toJson({source: true}).modules[0].source; 21 | expect(output).toBe(readJs('./examples/img.js.gen')); 22 | }); 23 | it('should inline img[src$=.svg] in multi-line', async () => { 24 | const stats = await compiler('./examples/img-multiline.html'); 25 | const output = stats.toJson({source: true}).modules[0].source; 26 | expect(output).toBe(readJs('./examples/img-multiline.js.gen')); 27 | }); 28 | it('should inline math[src]', async () => { 29 | const stats = await compiler('./examples/math-ml.html'); 30 | const output = stats.toJson({source: true}).modules[0].source; 31 | expect(output).toBe(readJs('./examples/math-ml.js.gen')); 32 | }); 33 | it('should ignore elements without markup-inline attribute', async () => { 34 | const stats = await compiler('./examples/basic-no-inline.html'); 35 | const output = stats.toJson({source: true}).modules[0].source; 36 | expect(output).toBe(readJs('./examples/basic-no-inline.js.gen')); 37 | }); 38 | it('should ignore other images', async () => { 39 | const stats = await compiler('./examples/non-svg-img.html'); 40 | const output = stats.toJson({source: true}).modules[0].source; 41 | expect(output).toBe(readJs('./examples/non-svg-img.js.gen')); 42 | }); 43 | it('should keep other attributes', async () => { 44 | const stats = await compiler('./examples/svg-with-attributes.html'); 45 | const output = stats.toJson({source: true}).modules[0].source; 46 | expect(output).toBe(readJs('./examples/svg-with-attributes.js.gen')); 47 | }); 48 | it('should auto svgo', async () => { 49 | const stats = await compiler('./examples/auto-svgo.html', {svgo: {plugins: [{removeTitle: false}]}}); 50 | const output = stats.toJson({source: true}).modules[0].source; 51 | expect(output).toBe(readJs('./examples/auto-svgo.js.gen')); 52 | }); 53 | it('should be allowed to customize strict selector', async () => { 54 | const stats = await compiler('./examples/custom-strict-selector.html', {strict: '[my-strict]'}); 55 | const output = stats.toJson({source: true}).modules[0].source; 56 | expect(output).toBe(readJs('./examples/custom-strict-selector.js.gen')); 57 | }); 58 | }); 59 | --------------------------------------------------------------------------------