Skip to content

Commit 8be6fdd

Browse files
feat: MathML support (#11387)
* feat: MathML support - Add support for MathML namespace - Auto-infer MathML namespace * tweak * DRY out * note to self --------- Co-authored-by: Rich Harris <[email protected]>
1 parent fe56c7f commit 8be6fdd

File tree

23 files changed

+302
-43
lines changed

23 files changed

+302
-43
lines changed

.changeset/cool-actors-tan.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
feat: MathML support

packages/svelte/src/compiler/phases/1-parse/read/options.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { namespace_svg } from '../../../../constants.js';
1+
import { namespace_mathml, namespace_svg } from '../../../../constants.js';
22
import * as e from '../../../errors.js';
33

44
const regex_valid_tag_name = /^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/;
@@ -155,10 +155,20 @@ export default function read_options(node) {
155155

156156
if (value === namespace_svg) {
157157
component_options.namespace = 'svg';
158-
} else if (value === 'html' || value === 'svg' || value === 'foreign') {
158+
} else if (value === namespace_mathml) {
159+
component_options.namespace = 'mathml';
160+
} else if (
161+
value === 'html' ||
162+
value === 'mathml' ||
163+
value === 'svg' ||
164+
value === 'foreign'
165+
) {
159166
component_options.namespace = value;
160167
} else {
161-
e.svelte_options_invalid_attribute_value(attribute, `"html", "svg" or "foreign"`);
168+
e.svelte_options_invalid_attribute_value(
169+
attribute,
170+
`"html", "mathml", "svg" or "foreign"`
171+
);
162172
}
163173

164174
break;

packages/svelte/src/compiler/phases/2-analyze/index.js

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ import {
1111
object
1212
} from '../../utils/ast.js';
1313
import * as b from '../../utils/builders.js';
14-
import { ReservedKeywords, Runes, SVGElements } from '../constants.js';
14+
import { MathMLElements, ReservedKeywords, Runes, SVGElements } from '../constants.js';
1515
import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js';
1616
import { merge } from '../visitors.js';
1717
import { validation_legacy, validation_runes, validation_runes_js } from './validation.js';
1818
import check_graph_for_cycles from './utils/check_graph_for_cycles.js';
1919
import { regex_starts_with_newline } from '../patterns.js';
2020
import { create_attribute, is_element_node } from '../nodes.js';
21-
import { DelegatedEvents, namespace_svg } from '../../../constants.js';
21+
import { DelegatedEvents, namespace_mathml, namespace_svg } from '../../../constants.js';
2222
import { should_proxy_or_freeze } from '../3-transform/client/utils.js';
2323
import { analyze_css } from './css/css-analyze.js';
2424
import { prune } from './css/css-prune.js';
@@ -1379,8 +1379,9 @@ const common_visitors = {
13791379
FunctionExpression: function_visitor,
13801380
FunctionDeclaration: function_visitor,
13811381
RegularElement(node, context) {
1382-
if (context.state.options.namespace !== 'foreign' && SVGElements.includes(node.name)) {
1383-
node.metadata.svg = true;
1382+
if (context.state.options.namespace !== 'foreign') {
1383+
if (SVGElements.includes(node.name)) node.metadata.svg = true;
1384+
else if (MathMLElements.includes(node.name)) node.metadata.mathml = true;
13841385
}
13851386

13861387
determine_element_spread(node);
@@ -1438,20 +1439,29 @@ const common_visitors = {
14381439
SvelteElement(node, context) {
14391440
context.state.analysis.elements.push(node);
14401441

1442+
// TODO why are we handling the `<svelte:element this="x" />` case? there is no
1443+
// reason for someone to use a static value with `<svelte:element>`
14411444
if (
14421445
context.state.options.namespace !== 'foreign' &&
14431446
node.tag.type === 'Literal' &&
1444-
typeof node.tag.value === 'string' &&
1445-
SVGElements.includes(node.tag.value)
1447+
typeof node.tag.value === 'string'
14461448
) {
1447-
node.metadata.svg = true;
1448-
return;
1449+
if (SVGElements.includes(node.tag.value)) {
1450+
node.metadata.svg = true;
1451+
return;
1452+
}
1453+
1454+
if (MathMLElements.includes(node.tag.value)) {
1455+
node.metadata.mathml = true;
1456+
return;
1457+
}
14491458
}
14501459

14511460
for (const attribute of node.attributes) {
14521461
if (attribute.type === 'Attribute') {
14531462
if (attribute.name === 'xmlns' && is_text_attribute(attribute)) {
14541463
node.metadata.svg = attribute.value[0].data === namespace_svg;
1464+
node.metadata.mathml = attribute.value[0].data === namespace_mathml;
14551465
return;
14561466
}
14571467
}
@@ -1467,13 +1477,18 @@ const common_visitors = {
14671477
) {
14681478
// Inside a slot or a snippet -> this resets the namespace, so assume the component namespace
14691479
node.metadata.svg = context.state.options.namespace === 'svg';
1480+
node.metadata.mathml = context.state.options.namespace === 'mathml';
14701481
return;
14711482
}
14721483
if (ancestor.type === 'SvelteElement' || ancestor.type === 'RegularElement') {
14731484
node.metadata.svg =
14741485
ancestor.type === 'RegularElement' && ancestor.name === 'foreignObject'
14751486
? false
14761487
: ancestor.metadata.svg;
1488+
node.metadata.mathml =
1489+
ancestor.type === 'RegularElement' && ancestor.name === 'foreignObject'
1490+
? false
1491+
: ancestor.metadata.mathml;
14771492
return;
14781493
}
14791494
}

packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@ import { walk } from 'zimmerframe';
5050
*/
5151
function get_attribute_name(element, attribute, context) {
5252
let name = attribute.name;
53-
if (!element.metadata.svg && context.state.metadata.namespace !== 'foreign') {
53+
if (
54+
!element.metadata.svg &&
55+
!element.metadata.mathml &&
56+
context.state.metadata.namespace !== 'foreign'
57+
) {
5458
name = name.toLowerCase();
5559
if (name in AttributeAliases) {
5660
name = AttributeAliases[name];
@@ -292,7 +296,9 @@ function serialize_element_spread_attributes(
292296
}
293297

294298
const lowercase_attributes =
295-
element.metadata.svg || is_custom_element_node(element) ? b.false : b.true;
299+
element.metadata.svg || element.metadata.mathml || is_custom_element_node(element)
300+
? b.false
301+
: b.true;
296302
const id = context.state.scope.generate('attributes');
297303

298304
const update = b.stmt(
@@ -465,6 +471,7 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu
465471
const state = context.state;
466472
const name = get_attribute_name(element, attribute, context);
467473
const is_svg = context.state.metadata.namespace === 'svg';
474+
const is_mathml = context.state.metadata.namespace === 'mathml';
468475
let [contains_call_expression, value] = serialize_attribute_value(attribute.value, context);
469476

470477
// The foreign namespace doesn't have any special handling, everything goes through the attr function
@@ -490,7 +497,13 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu
490497
let update;
491498

492499
if (name === 'class') {
493-
update = b.stmt(b.call(is_svg ? '$.set_svg_class' : '$.set_class', node_id, value));
500+
update = b.stmt(
501+
b.call(
502+
is_svg ? '$.set_svg_class' : is_mathml ? '$.set_mathml_class' : '$.set_class',
503+
node_id,
504+
value
505+
)
506+
);
494507
} else if (DOMProperties.includes(name)) {
495508
update = b.stmt(b.assignment('=', b.member(node_id, b.id(name)), value));
496509
} else {
@@ -872,7 +885,9 @@ function serialize_inline_component(node, component_name, context) {
872885
node_id,
873886
// TODO would be great to do this at runtime instead. Svelte 4 also can't handle cases today
874887
// where it's not statically determinable whether the component is used in a svg or html context
875-
context.state.metadata.namespace === 'svg' ? b.false : b.true,
888+
context.state.metadata.namespace === 'svg' || context.state.metadata.namespace === 'mathml'
889+
? b.false
890+
: b.true,
876891
b.thunk(b.object(custom_css_props)),
877892
b.arrow([b.id('$$node')], prev(b.id('$$node')))
878893
);
@@ -1138,9 +1153,11 @@ function get_template_function(namespace, state) {
11381153
? contains_script_tag
11391154
? '$.svg_template_with_script'
11401155
: '$.svg_template'
1141-
: contains_script_tag
1142-
? '$.template_with_script'
1143-
: '$.template';
1156+
: namespace === 'mathml'
1157+
? '$.mathml_template'
1158+
: contains_script_tag
1159+
? '$.template_with_script'
1160+
: '$.template';
11441161
}
11451162

11461163
/**
@@ -1635,7 +1652,8 @@ export const template_visitors = {
16351652
'$.html',
16361653
context.state.node,
16371654
b.thunk(/** @type {import('estree').Expression} */ (context.visit(node.expression))),
1638-
b.literal(context.state.metadata.namespace === 'svg')
1655+
b.literal(context.state.metadata.namespace === 'svg'),
1656+
b.literal(context.state.metadata.namespace === 'mathml')
16391657
)
16401658
)
16411659
);
@@ -2125,7 +2143,11 @@ export const template_visitors = {
21252143
})
21262144
);
21272145

2128-
const args = [context.state.node, get_tag, node.metadata.svg ? b.true : b.false];
2146+
const args = [
2147+
context.state.node,
2148+
get_tag,
2149+
node.metadata.svg || node.metadata.mathml ? b.true : b.false
2150+
];
21292151
if (inner.length > 0) {
21302152
args.push(b.arrow([element_id, b.id('$$anchor')], b.block(inner)));
21312153
}

packages/svelte/src/compiler/phases/3-transform/server/transform-server.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,11 @@ function serialize_set_binding(node, context, fallback) {
484484
*/
485485
function get_attribute_name(element, attribute, context) {
486486
let name = attribute.name;
487-
if (!element.metadata.svg && context.state.metadata.namespace !== 'foreign') {
487+
if (
488+
!element.metadata.svg &&
489+
!element.metadata.mathml &&
490+
context.state.metadata.namespace !== 'foreign'
491+
) {
488492
name = name.toLowerCase();
489493
// don't lookup boolean aliases here, the server runtime function does only
490494
// check for the lowercase variants of boolean attributes
@@ -899,15 +903,19 @@ function serialize_element_spread_attributes(
899903
}
900904

901905
const lowercase_attributes =
902-
element.metadata.svg || (element.type === 'RegularElement' && is_custom_element_node(element))
906+
element.metadata.svg ||
907+
element.metadata.mathml ||
908+
(element.type === 'RegularElement' && is_custom_element_node(element))
903909
? b.false
904910
: b.true;
905-
const is_svg = element.metadata.svg ? b.true : b.false;
911+
912+
const is_html = element.metadata.svg || element.metadata.mathml ? b.false : b.true;
913+
906914
/** @type {import('estree').Expression[]} */
907915
const args = [
908916
b.array(values),
909917
lowercase_attributes,
910-
is_svg,
918+
is_html,
911919
b.literal(context.state.analysis.css.hash)
912920
];
913921

packages/svelte/src/compiler/phases/3-transform/utils.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,10 @@ export function infer_namespace(namespace, parent, nodes, path) {
219219
}
220220

221221
if (parent_node?.type === 'RegularElement' || parent_node?.type === 'SvelteElement') {
222-
return parent_node.metadata.svg ? 'svg' : 'html';
222+
if (parent_node.metadata.svg) {
223+
return 'svg';
224+
}
225+
return parent_node.metadata.mathml ? 'mathml' : 'html';
223226
}
224227

225228
// Re-evaluate the namespace inside slot nodes that reset the namespace
@@ -254,11 +257,11 @@ function check_nodes_for_namespace(nodes, namespace) {
254257
* @param {{stop: () => void}} context
255258
*/
256259
const RegularElement = (node, { stop }) => {
257-
if (!node.metadata.svg) {
260+
if (!node.metadata.svg && !node.metadata.mathml) {
258261
namespace = 'html';
259262
stop();
260263
} else if (namespace === 'keep') {
261-
namespace = 'svg';
264+
namespace = node.metadata.svg ? 'svg' : 'mathml';
262265
}
263266
};
264267

@@ -312,7 +315,11 @@ export function determine_namespace_for_children(node, namespace) {
312315
return 'html';
313316
}
314317

315-
return node.metadata.svg ? 'svg' : 'html';
318+
if (node.metadata.svg) {
319+
return 'svg';
320+
}
321+
322+
return node.metadata.mathml ? 'mathml' : 'html';
316323
}
317324

318325
/**

packages/svelte/src/compiler/phases/constants.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,39 @@ export const SVGElements = [
149149
'vkern'
150150
];
151151

152+
export const MathMLElements = [
153+
'annotation',
154+
'annotation-xml',
155+
'maction',
156+
'math',
157+
'merror',
158+
'mfrac',
159+
'mi',
160+
'mmultiscripts',
161+
'mn',
162+
'mo',
163+
'mover',
164+
'mpadded',
165+
'mphantom',
166+
'mprescripts',
167+
'mroot',
168+
'mrow',
169+
'ms',
170+
'mspace',
171+
'msqrt',
172+
'mstyle',
173+
'msub',
174+
'msubsup',
175+
'msup',
176+
'mtable',
177+
'mtd',
178+
'mtext',
179+
'mtr',
180+
'munder',
181+
'munderover',
182+
'semantics'
183+
];
184+
152185
export const EventModifiers = [
153186
'preventDefault',
154187
'stopPropagation',

packages/svelte/src/compiler/types/template.d.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,12 @@ export interface Fragment {
3939
/**
4040
* - `html` — the default, for e.g. `<div>` or `<span>`
4141
* - `svg` — for e.g. `<svg>` or `<g>`
42+
* - `mathml` — for e.g. `<math>` or `<mrow>`
4243
* - `foreign` — for other compilation targets than the web, e.g. Svelte Native.
4344
* Disallows bindings other than bind:this, disables a11y checks, disables any special attribute handling
4445
* (also see https://github.com/sveltejs/svelte/pull/5652)
4546
*/
46-
export type Namespace = 'html' | 'svg' | 'foreign';
47+
export type Namespace = 'html' | 'svg' | 'mathml' | 'foreign';
4748

4849
export interface Root extends BaseNode {
4950
type: 'Root';
@@ -287,6 +288,8 @@ export interface RegularElement extends BaseElement {
287288
metadata: {
288289
/** `true` if this is an svg element */
289290
svg: boolean;
291+
/** `true` if this is a mathml element */
292+
mathml: boolean;
290293
/** `true` if contains a SpreadAttribute */
291294
has_spread: boolean;
292295
scoped: boolean;
@@ -319,6 +322,11 @@ export interface SvelteElement extends BaseElement {
319322
* the tag is dynamic, but we do our best to infer it from the template.
320323
*/
321324
svg: boolean;
325+
/**
326+
* `true` if this is a mathml element. The boolean may not be accurate because
327+
* the tag is dynamic, but we do our best to infer it from the template.
328+
*/
329+
mathml: boolean;
322330
scoped: boolean;
323331
};
324332
}

packages/svelte/src/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export const DOMBooleanAttributes = [
104104
];
105105

106106
export const namespace_svg = 'http://www.w3.org/2000/svg';
107+
export const namespace_mathml = 'http://www.w3.org/1998/Math/MathML';
107108

108109
// while `input` is also an interactive element, it is never moved by the browser, so we don't need to check for it
109110
export const interactive_elements = new Set([

0 commit comments

Comments
 (0)