├── .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 |
120 | ```
121 |
122 | So we can apply css styles to `svg > path {}`.
123 |
124 | or
125 |
126 | ```svg
127 |
128 |
129 |
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 = " ";
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 = "