html-minifier-next 4.12.2 → 4.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,213 @@
1
+ // RegExp patterns (to avoid repeated allocations in hot paths)
2
+
3
+ const RE_WS_START = /^[ \n\r\t\f]+/;
4
+ const RE_WS_END = /[ \n\r\t\f]+$/;
5
+ const RE_ALL_WS_NBSP = /[ \n\r\t\f\xA0]+/g;
6
+ const RE_NBSP_LEADING_GROUP = /(^|\xA0+)[^\xA0]+/g;
7
+ const RE_NBSP_LEAD_GROUP = /(\xA0+)[^\xA0]+/g;
8
+ const RE_NBSP_TRAILING_GROUP = /[^\xA0]+(\xA0+)/g;
9
+ const RE_NBSP_TRAILING_STRIP = /[^\xA0]+$/;
10
+ const RE_CONDITIONAL_COMMENT = /^\[if\s[^\]]+]|\[endif]$/;
11
+ const RE_EVENT_ATTR_DEFAULT = /^on[a-z]{3,}$/;
12
+ const RE_CAN_REMOVE_ATTR_QUOTES = /^[^ \t\n\f\r"'`=<>]+$/;
13
+ const RE_TRAILING_SEMICOLON = /;$/;
14
+ const RE_AMP_ENTITY = /&(#?[0-9a-zA-Z]+;)/g;
15
+
16
+ // Inline element Sets for whitespace handling
17
+
18
+ // Non-empty elements that will maintain whitespace around them
19
+ const inlineElementsToKeepWhitespaceAround = new Set(['a', 'abbr', 'acronym', 'b', 'bdi', 'bdo', 'big', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'mark', 'math', 'meter', 'nobr', 'object', 'output', 'progress', 'q', 'rb', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'svg', 'textarea', 'time', 'tt', 'u', 'var', 'wbr']);
20
+
21
+ // Non-empty elements that will maintain whitespace within them
22
+ const inlineElementsToKeepWhitespaceWithin = new Set(['a', 'abbr', 'acronym', 'b', 'big', 'del', 'em', 'font', 'i', 'ins', 'kbd', 'mark', 'nobr', 's', 'samp', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'time', 'tt', 'u', 'var']);
23
+
24
+ // Elements that will always maintain whitespace around them
25
+ const inlineElementsToKeepWhitespace = new Set(['comment', 'img', 'input', 'wbr']);
26
+
27
+ // Default attribute values
28
+
29
+ // Default attribute values (could apply to any element)
30
+ const generalDefaults = {
31
+ autocorrect: 'on',
32
+ fetchpriority: 'auto',
33
+ loading: 'eager',
34
+ popovertargetaction: 'toggle'
35
+ };
36
+
37
+ // Tag-specific default attribute values
38
+ const tagDefaults = {
39
+ area: { shape: 'rect' },
40
+ button: { type: 'submit' },
41
+ form: {
42
+ enctype: 'application/x-www-form-urlencoded',
43
+ method: 'get'
44
+ },
45
+ html: { dir: 'ltr' },
46
+ img: { decoding: 'auto' },
47
+ input: {
48
+ colorspace: 'limited-srgb',
49
+ type: 'text'
50
+ },
51
+ marquee: {
52
+ behavior: 'scroll',
53
+ direction: 'left'
54
+ },
55
+ style: { media: 'all' },
56
+ textarea: { wrap: 'soft' },
57
+ track: { kind: 'subtitles' }
58
+ };
59
+
60
+ // Script MIME types
61
+
62
+ // https://mathiasbynens.be/demo/javascript-mime-type
63
+ // https://developer.mozilla.org/en/docs/Web/HTML/Element/script#attr-type
64
+ const executableScriptsMimetypes = new Set([
65
+ 'text/javascript',
66
+ 'text/ecmascript',
67
+ 'text/jscript',
68
+ 'application/javascript',
69
+ 'application/x-javascript',
70
+ 'application/ecmascript',
71
+ 'module'
72
+ ]);
73
+
74
+ const keepScriptsMimetypes = new Set([
75
+ 'module'
76
+ ]);
77
+
78
+ // Boolean attribute Sets
79
+
80
+ const isSimpleBoolean = new Set(['allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked', 'compact', 'controls', 'declare', 'default', 'defaultchecked', 'defaultmuted', 'defaultselected', 'defer', 'disabled', 'enabled', 'formnovalidate', 'hidden', 'indeterminate', 'inert', 'ismap', 'itemscope', 'loop', 'multiple', 'muted', 'nohref', 'noresize', 'noshade', 'novalidate', 'nowrap', 'open', 'pauseonexit', 'readonly', 'required', 'reversed', 'scoped', 'seamless', 'selected', 'sortable', 'truespeed', 'typemustmatch', 'visible']);
81
+
82
+ const isBooleanValue = new Set(['true', 'false']);
83
+
84
+ // `srcset` tags
85
+
86
+ const srcsetTags = new Set(['img', 'source']);
87
+
88
+ // JSON script types
89
+
90
+ const jsonScriptTypes = new Set([
91
+ 'application/json',
92
+ 'application/ld+json',
93
+ 'application/manifest+json',
94
+ 'application/vnd.geo+json',
95
+ 'application/problem+json',
96
+ 'application/merge-patch+json',
97
+ 'application/json-patch+json',
98
+ 'importmap',
99
+ 'speculationrules',
100
+ ]);
101
+
102
+ // Tag omission rules and element Sets
103
+
104
+ // Tag omission rules from https://html.spec.whatwg.org/multipage/syntax.html#optional-tags with the following extensions:
105
+ // - retain `<body>` if followed by `<noscript>`
106
+ // - `<rb>`, `<rt>`, `<rtc>`, `<rp>` follow HTML Ruby Markup Extensions draft (https://www.w3.org/TR/html-ruby-extensions/)
107
+ // - retain all tags which are adjacent to non-standard HTML tags
108
+
109
+ const optionalStartTags = new Set(['html', 'head', 'body', 'colgroup', 'tbody']);
110
+
111
+ const optionalEndTags = new Set(['html', 'head', 'body', 'li', 'dt', 'dd', 'p', 'rb', 'rt', 'rtc', 'rp', 'optgroup', 'option', 'colgroup', 'caption', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th']);
112
+
113
+ const headerTags = new Set(['meta', 'link', 'script', 'style', 'template', 'noscript']);
114
+
115
+ const descriptionTags = new Set(['dt', 'dd']);
116
+
117
+ const pBlockTags = new Set(['address', 'article', 'aside', 'blockquote', 'details', 'dialog', 'div', 'dl', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'main', 'menu', 'nav', 'ol', 'p', 'pre', 'search', 'section', 'table', 'ul']);
118
+
119
+ const pInlineTags = new Set(['a', 'audio', 'del', 'ins', 'map', 'noscript', 'video']);
120
+
121
+ const rubyEndTagOmission = new Set(['rb', 'rt', 'rtc', 'rp']); // `</rb>`, `</rt>`, `</rp>` can be omitted if followed by `<rb>`, `<rt>`, `<rtc>`, or `<rp>`
122
+
123
+ const rubyRtcEndTagOmission = new Set(['rb', 'rtc']); // `</rtc>` can be omitted if followed by `<rb>` or `<rtc>` (not `<rt>` or `<rp>`)
124
+
125
+ const optionTag = new Set(['option', 'optgroup']);
126
+
127
+ const tableContentTags = new Set(['tbody', 'tfoot']);
128
+
129
+ const tableSectionTags = new Set(['thead', 'tbody', 'tfoot']);
130
+
131
+ const cellTags = new Set(['td', 'th']);
132
+
133
+ const topLevelTags = new Set(['html', 'head', 'body']);
134
+
135
+ const compactTags = new Set(['html', 'body']);
136
+
137
+ const looseTags = new Set(['head', 'colgroup', 'caption']);
138
+
139
+ const trailingTags = new Set(['dt', 'thead']);
140
+
141
+ const htmlTags = new Set(['a', 'abbr', 'acronym', 'address', 'applet', 'area', 'article', 'aside', 'audio', 'b', 'base', 'basefont', 'bdi', 'bdo', 'bgsound', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'command', 'content', 'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe', 'image', 'img', 'input', 'ins', 'isindex', 'kbd', 'keygen', 'label', 'legend', 'li', 'link', 'listing', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meta', 'meter', 'multicol', 'nav', 'nobr', 'noembed', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'picture', 'plaintext', 'pre', 'progress', 'q', 'rb', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'script', 'search', 'section', 'select', 'selectedcontent', 'shadow', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr', 'xmp']);
142
+
143
+ // Empty attribute regex
144
+
145
+ const reEmptyAttribute = new RegExp(
146
+ '^(?:class|id|style|title|lang|dir|on(?:focus|blur|change|click|dblclick|mouse(' +
147
+ '?:down|up|over|move|out)|key(?:press|down|up)))$');
148
+
149
+ // Special content elements
150
+
151
+ const specialContentTags = new Set(['script', 'style']);
152
+
153
+ // Exports
154
+
155
+ export {
156
+ // RegExp patterns
157
+ RE_WS_START,
158
+ RE_WS_END,
159
+ RE_ALL_WS_NBSP,
160
+ RE_NBSP_LEADING_GROUP,
161
+ RE_NBSP_LEAD_GROUP,
162
+ RE_NBSP_TRAILING_GROUP,
163
+ RE_NBSP_TRAILING_STRIP,
164
+ RE_CONDITIONAL_COMMENT,
165
+ RE_EVENT_ATTR_DEFAULT,
166
+ RE_CAN_REMOVE_ATTR_QUOTES,
167
+ RE_TRAILING_SEMICOLON,
168
+ RE_AMP_ENTITY,
169
+
170
+ // Inline element Sets
171
+ inlineElementsToKeepWhitespaceAround,
172
+ inlineElementsToKeepWhitespaceWithin,
173
+ inlineElementsToKeepWhitespace,
174
+
175
+ // Default values
176
+ generalDefaults,
177
+ tagDefaults,
178
+
179
+ // Script/style constants
180
+ executableScriptsMimetypes,
181
+ keepScriptsMimetypes,
182
+ jsonScriptTypes,
183
+
184
+ // Boolean Sets
185
+ isSimpleBoolean,
186
+ isBooleanValue,
187
+
188
+ // Misc
189
+ srcsetTags,
190
+
191
+ // Tag omission rules
192
+ optionalStartTags,
193
+ optionalEndTags,
194
+ headerTags,
195
+ descriptionTags,
196
+ pBlockTags,
197
+ pInlineTags,
198
+ rubyEndTagOmission,
199
+ rubyRtcEndTagOmission,
200
+ optionTag,
201
+ tableContentTags,
202
+ tableSectionTags,
203
+ cellTags,
204
+ topLevelTags,
205
+ compactTags,
206
+ looseTags,
207
+ trailingTags,
208
+ htmlTags,
209
+
210
+ // Regex
211
+ reEmptyAttribute,
212
+ specialContentTags
213
+ };
@@ -0,0 +1,105 @@
1
+ // Imports
2
+
3
+ import {
4
+ jsonScriptTypes
5
+ } from './constants.js';
6
+ import { replaceAsync } from './utils.js';
7
+ import { trimWhitespace } from './whitespace.js';
8
+
9
+ // CSS processing
10
+
11
+ // Wrap CSS declarations for inline styles and media queries
12
+ // This ensures proper context for CSS minification
13
+
14
+ function wrapCSS(text, type) {
15
+ switch (type) {
16
+ case 'inline':
17
+ return '*{' + text + '}';
18
+ case 'media':
19
+ return '@media ' + text + '{a{top:0}}';
20
+ default:
21
+ return text;
22
+ }
23
+ }
24
+
25
+ function unwrapCSS(text, type) {
26
+ let matches;
27
+ switch (type) {
28
+ case 'inline':
29
+ matches = text.match(/^\*\{([\s\S]*)\}$/);
30
+ break;
31
+ case 'media':
32
+ matches = text.match(/^@media ([\s\S]*?)\s*{[\s\S]*}$/);
33
+ break;
34
+ }
35
+ return matches ? matches[1] : text;
36
+ }
37
+
38
+ async function cleanConditionalComment(comment, options, minifyHTML) {
39
+ return options.processConditionalComments
40
+ ? await replaceAsync(comment, /^(\[if\s[^\]]+]>)([\s\S]*?)(<!\[endif])$/, async function (match, prefix, text, suffix) {
41
+ return prefix + await minifyHTML(text, options, true) + suffix;
42
+ })
43
+ : comment;
44
+ }
45
+
46
+ // Script processing
47
+
48
+ function minifyJson(text, options) {
49
+ try {
50
+ return JSON.stringify(JSON.parse(text));
51
+ }
52
+ catch (err) {
53
+ if (!options.continueOnMinifyError) {
54
+ throw err;
55
+ }
56
+ options.log && options.log(err);
57
+ return text;
58
+ }
59
+ }
60
+
61
+ function hasJsonScriptType(attrs) {
62
+ for (let i = 0, len = attrs.length; i < len; i++) {
63
+ const attrName = attrs[i].name.toLowerCase();
64
+ if (attrName === 'type') {
65
+ const attrValue = trimWhitespace((attrs[i].value || '').split(/;/, 2)[0]).toLowerCase();
66
+ if (jsonScriptTypes.has(attrValue)) {
67
+ return true;
68
+ }
69
+ }
70
+ }
71
+ return false;
72
+ }
73
+
74
+ async function processScript(text, options, currentAttrs, minifyHTML) {
75
+ for (let i = 0, len = currentAttrs.length; i < len; i++) {
76
+ const attrName = currentAttrs[i].name.toLowerCase();
77
+ if (attrName === 'type') {
78
+ const rawValue = currentAttrs[i].value;
79
+ const normalizedValue = trimWhitespace((rawValue || '').split(/;/, 2)[0]).toLowerCase();
80
+ // Minify JSON script types automatically
81
+ if (jsonScriptTypes.has(normalizedValue)) {
82
+ return minifyJson(text, options);
83
+ }
84
+ // Process custom script types if specified
85
+ if (options.processScripts && options.processScripts.indexOf(rawValue) > -1) {
86
+ return await minifyHTML(text, options);
87
+ }
88
+ }
89
+ }
90
+ return text;
91
+ }
92
+
93
+ // Exports
94
+
95
+ export {
96
+ // CSS
97
+ wrapCSS,
98
+ unwrapCSS,
99
+ cleanConditionalComment,
100
+
101
+ // Scripts
102
+ minifyJson,
103
+ hasJsonScriptType,
104
+ processScript
105
+ };
@@ -0,0 +1,242 @@
1
+ // Imports
2
+
3
+ import {
4
+ headerTags,
5
+ descriptionTags,
6
+ pBlockTags,
7
+ rubyEndTagOmission,
8
+ rubyRtcEndTagOmission,
9
+ optionTag,
10
+ tableContentTags,
11
+ tableSectionTags,
12
+ cellTags
13
+ } from './constants.js';
14
+ import { hasAttrName } from './attributes.js';
15
+
16
+ // Tag omission rules
17
+
18
+ function canRemoveParentTag(optionalStartTag, tag) {
19
+ switch (optionalStartTag) {
20
+ case 'html':
21
+ case 'head':
22
+ return true;
23
+ case 'body':
24
+ return !headerTags.has(tag);
25
+ case 'colgroup':
26
+ return tag === 'col';
27
+ case 'tbody':
28
+ return tag === 'tr';
29
+ }
30
+ return false;
31
+ }
32
+
33
+ function isStartTagMandatory(optionalEndTag, tag) {
34
+ switch (tag) {
35
+ case 'colgroup':
36
+ return optionalEndTag === 'colgroup';
37
+ case 'tbody':
38
+ return tableSectionTags.has(optionalEndTag);
39
+ }
40
+ return false;
41
+ }
42
+
43
+ function canRemovePrecedingTag(optionalEndTag, tag) {
44
+ switch (optionalEndTag) {
45
+ case 'html':
46
+ case 'head':
47
+ case 'body':
48
+ case 'colgroup':
49
+ case 'caption':
50
+ return true;
51
+ case 'li':
52
+ case 'optgroup':
53
+ case 'tr':
54
+ return tag === optionalEndTag;
55
+ case 'dt':
56
+ case 'dd':
57
+ return descriptionTags.has(tag);
58
+ case 'p':
59
+ return pBlockTags.has(tag);
60
+ case 'rb':
61
+ case 'rt':
62
+ case 'rp':
63
+ return rubyEndTagOmission.has(tag);
64
+ case 'rtc':
65
+ return rubyRtcEndTagOmission.has(tag);
66
+ case 'option':
67
+ return optionTag.has(tag);
68
+ case 'thead':
69
+ case 'tbody':
70
+ return tableContentTags.has(tag);
71
+ case 'tfoot':
72
+ return tag === 'tbody';
73
+ case 'td':
74
+ case 'th':
75
+ return cellTags.has(tag);
76
+ }
77
+ return false;
78
+ }
79
+
80
+ // Element removal logic
81
+
82
+ function canRemoveElement(tag, attrs) {
83
+ switch (tag) {
84
+ case 'textarea':
85
+ return false;
86
+ case 'audio':
87
+ case 'script':
88
+ case 'video':
89
+ if (hasAttrName('src', attrs)) {
90
+ return false;
91
+ }
92
+ break;
93
+ case 'iframe':
94
+ if (hasAttrName('src', attrs) || hasAttrName('srcdoc', attrs)) {
95
+ return false;
96
+ }
97
+ break;
98
+ case 'object':
99
+ if (hasAttrName('data', attrs)) {
100
+ return false;
101
+ }
102
+ break;
103
+ case 'applet':
104
+ if (hasAttrName('code', attrs)) {
105
+ return false;
106
+ }
107
+ break;
108
+ }
109
+ return true;
110
+ }
111
+
112
+ /**
113
+ * @param {string} str - Tag name or HTML-like element spec (e.g., “td” or “<span aria-hidden='true'>”)
114
+ * @param {MinifierOptions} options - Options object for name normalization
115
+ * @returns {{tag: string, attrs: Object.<string, string|undefined>|null}|null} Parsed spec or null if invalid
116
+ */
117
+ function parseElementSpec(str, options) {
118
+ if (typeof str !== 'string') {
119
+ return null;
120
+ }
121
+
122
+ const trimmed = str.trim();
123
+ if (!trimmed) {
124
+ return null;
125
+ }
126
+
127
+ // Simple tag name: `td`
128
+ if (!/[<>]/.test(trimmed)) {
129
+ return { tag: options.name(trimmed), attrs: null };
130
+ }
131
+
132
+ // HTML-like markup: `<span aria-hidden='true'>` or `<td></td>`
133
+ // Extract opening tag using regex
134
+ const match = trimmed.match(/^<([a-zA-Z][\w:-]*)((?:\s+[^>]*)?)>/);
135
+ if (!match) {
136
+ return null;
137
+ }
138
+
139
+ const tag = options.name(match[1]);
140
+ const attrString = match[2];
141
+
142
+ if (!attrString.trim()) {
143
+ return { tag, attrs: null };
144
+ }
145
+
146
+ // Parse attributes from string
147
+ const attrs = {};
148
+ const attrRegex = /([a-zA-Z][\w:-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>/]+)))?/g;
149
+ let attrMatch;
150
+
151
+ while ((attrMatch = attrRegex.exec(attrString))) {
152
+ const attrName = options.name(attrMatch[1]);
153
+ const attrValue = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4];
154
+ // Boolean attributes have no value (undefined)
155
+ attrs[attrName] = attrValue;
156
+ }
157
+
158
+ return {
159
+ tag,
160
+ attrs: Object.keys(attrs).length > 0 ? attrs : null
161
+ };
162
+ }
163
+
164
+ /**
165
+ * @param {string[]} input - Array of element specifications from `removeEmptyElementsExcept` option
166
+ * @param {MinifierOptions} options - Options object for parsing
167
+ * @returns {Array<{tag: string, attrs: Object.<string, string|undefined>|null}>} Array of parsed element specs
168
+ */
169
+ function parseRemoveEmptyElementsExcept(input, options) {
170
+ if (!Array.isArray(input)) {
171
+ return [];
172
+ }
173
+
174
+ return input.map(item => {
175
+ if (typeof item === 'string') {
176
+ const spec = parseElementSpec(item, options);
177
+ if (!spec && options.log) {
178
+ options.log('Warning: Unable to parse “removeEmptyElementsExcept” specification: "' + item + '"');
179
+ }
180
+ return spec;
181
+ }
182
+ if (options.log) {
183
+ options.log('Warning: “removeEmptyElementsExcept” specification must be a string, received: ' + typeof item);
184
+ }
185
+ return null;
186
+ }).filter(Boolean);
187
+ }
188
+
189
+ /**
190
+ * @param {string} tag - Element tag name
191
+ * @param {HTMLAttribute[]} attrs - Array of element attributes
192
+ * @param {Array<{tag: string, attrs: Object.<string, string|undefined>|null}>} preserveList - Parsed preserve specs
193
+ * @returns {boolean} True if the empty element should be preserved
194
+ */
195
+ function shouldPreserveEmptyElement(tag, attrs, preserveList) {
196
+ for (const spec of preserveList) {
197
+ // Tag name must match
198
+ if (spec.tag !== tag) {
199
+ continue;
200
+ }
201
+
202
+ // If no attributes specified in spec, tag match is enough
203
+ if (!spec.attrs) {
204
+ return true;
205
+ }
206
+
207
+ // Check if all specified attributes match
208
+ const allAttrsMatch = Object.entries(spec.attrs).every(([name, value]) => {
209
+ const attr = attrs.find(a => a.name === name);
210
+ if (!attr) {
211
+ return false; // Attribute not present
212
+ }
213
+ // Boolean attribute in spec (undefined value) matches if attribute is present
214
+ if (value === undefined) {
215
+ return true;
216
+ }
217
+ // Valued attribute must match exactly
218
+ return attr.value === value;
219
+ });
220
+
221
+ if (allAttrsMatch) {
222
+ return true;
223
+ }
224
+ }
225
+
226
+ return false;
227
+ }
228
+
229
+ // Exports
230
+
231
+ export {
232
+ // Tag omission
233
+ canRemoveParentTag,
234
+ isStartTagMandatory,
235
+ canRemovePrecedingTag,
236
+
237
+ // Element removal
238
+ canRemoveElement,
239
+ parseElementSpec,
240
+ parseRemoveEmptyElementsExcept,
241
+ shouldPreserveEmptyElement
242
+ };
@@ -0,0 +1,20 @@
1
+ // Utils
2
+ export * from './utils.js';
3
+
4
+ // Constants
5
+ export * from './constants.js';
6
+
7
+ // Whitespace
8
+ export * from './whitespace.js';
9
+
10
+ // Attributes
11
+ export * from './attributes.js';
12
+
13
+ // Elements
14
+ export * from './elements.js';
15
+
16
+ // Content processors
17
+ export * from './content.js';
18
+
19
+ // Options
20
+ export * from './options.js';