@uniweb/press 0.4.13 → 0.4.15

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/press",
3
- "version": "0.4.13",
3
+ "version": "0.4.15",
4
4
  "description": "Press your React components into downloadable files — Word, Excel, PDF. Registration-based pattern with lazy-loaded format adapters.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -609,6 +609,10 @@ function irToTextRunPair(node) {
609
609
  if (node.smallCaps === 'true' || node.smallCaps === true) options.smallCaps = true
610
610
  if (node.allCaps === 'true' || node.allCaps === true) options.allCaps = true
611
611
  if (node.strike === 'true' || node.strike === true) options.strike = true
612
+ // Subscript / superscript — mutually exclusive vertical alignment.
613
+ if (node.subScript === 'true' || node.subScript === true) options.subScript = true
614
+ else if (node.superScript === 'true' || node.superScript === true)
615
+ options.superScript = true
612
616
 
613
617
  result.push(new TextRun(options))
614
618
  return result
@@ -31,7 +31,7 @@
31
31
  import JSZip from 'jszip'
32
32
  import { parseFragment } from 'parse5'
33
33
  import { fetchAssets } from '../assets/fetch.js'
34
- import { MATH_CSS } from './math-css.js'
34
+ import { MATH_CSS, numberEquations } from './math-css.js'
35
35
 
36
36
  // ============================================================================
37
37
  // Public API
@@ -77,7 +77,16 @@ export async function compileEpub(input, options = {}) {
77
77
  // Walk each HTML fragment, collect image URLs, capture a chapter title.
78
78
  // The parsed tree is kept so we can re-serialize as XHTML after
79
79
  // rewriting <img src> to manifest-relative paths.
80
- const chapters = sections.map((html, i) => buildChapter(html, i))
80
+ // Numbered straight through the book rather than restarting per chapter,
81
+ // and as text rather than a CSS counter — reader support for counters is
82
+ // not something an EPUB can find out.
83
+ let eqnNo = 1
84
+ const numbered = sections.map((html) => {
85
+ const result = numberEquations(html, eqnNo)
86
+ eqnNo = result.next
87
+ return result.html
88
+ })
89
+ const chapters = numbered.map((html, i) => buildChapter(html, i))
81
90
 
82
91
  // Deduplicate image URLs across all chapters — and the cover, if any —
83
92
  // and fetch them in parallel. Failed fetches are logged and the original
@@ -43,12 +43,21 @@ export const MATH_CSS = `/* Temml's own class hooks — without these an aligned
43
43
  MathML-Core default here; Temml's own CSS only adjusts it for jot. */
44
44
  math mtd { padding-top: 0.5ex; padding-bottom: 0.5ex; }
45
45
  math mtable.tml-jot mtd { padding-top: 0.7ex; padding-bottom: 0.7ex; }
46
- /* AMS auto-numbering. Which equations number is the AUTHOR's choice, made in
46
+ /* AMS auto-numbering, for lanes that keep our CSS counter.
47
+
48
+ Scoped to :empty because the document lanes cannot rely on it -- Paged.js
49
+ rewrites counters for its own pagination and strips counter-increment, so
50
+ every equation rendered as "(0)" (measured 2026-07-31; the declaration
51
+ survives intact without the polyfill). press therefore writes the numbers
52
+ into the spans as text, and a span carrying a number is no longer :empty, so
53
+ the two can never both fire.
54
+
55
+ AMS auto-numbering. Which equations number is the AUTHOR's choice, made in
47
56
  LaTeX: align and equation number, aligned and the starred forms do not.
48
57
  Without these two rules that choice was discarded -- align and align-star
49
58
  rendered identically, so an author who asked for numbers silently got none.
50
59
  (No backticks in here: this string is a JS template literal.) */
51
- .tml-eqn::before {
60
+ .tml-eqn:empty::before {
52
61
  counter-increment: tmlEqnNo;
53
62
  content: "(" counter(tmlEqnNo) ")";
54
63
  }
@@ -74,3 +83,35 @@ math.tml-display:has(.tml-eqn) {
74
83
  display: block !important;
75
84
  width: 100%;
76
85
  }`
86
+
87
+ /**
88
+ * Write equation numbers into Temml's tag spans as text.
89
+ *
90
+ * Temml leaves `<span class="tml-eqn"></span>` empty and expects a CSS counter
91
+ * to draw the number. That works in a browser and not in a document: Paged.js
92
+ * rewrites counters for its own pagination and strips `counter-increment`, so
93
+ * every equation renders as "(0)" -- measured, and confirmed by the declaration
94
+ * surviving intact once the polyfill is removed. EPUB readers vary at least as
95
+ * widely, and neither lane offers a way to find out.
96
+ *
97
+ * So the document lanes stop asking CSS to count. A span that carries its
98
+ * number is no longer `:empty`, and the stylesheet's counter rule is scoped to
99
+ * `:empty`, so the two can never both fire and double up.
100
+ *
101
+ * A regex rather than a parse: Temml emits this span in exactly one shape, and
102
+ * the epub adapter would otherwise have to thread a counter through its parse5
103
+ * tree for no gain.
104
+ *
105
+ * @param {string} html
106
+ * @param {number} [start=1] - First number to use; lets a caller continue the
107
+ * sequence across sections so a book numbers straight through.
108
+ * @returns {{ html: string, next: number }}
109
+ */
110
+ export function numberEquations(html, start = 1) {
111
+ let n = start
112
+ const out = String(html ?? '').replace(
113
+ /<span class="tml-eqn"><\/span>/g,
114
+ () => `<span class="tml-eqn">(${n++})</span>`,
115
+ )
116
+ return { html: out, next: n }
117
+ }
@@ -129,6 +129,11 @@ async function compileServerSide(htmlDoc, options = {}) {
129
129
  * want to inspect the emitted document before handing it to a browser.
130
130
  */
131
131
  export function emitDocument({ body = '', meta = {}, stylesheet, polyfillUrl } = {}) {
132
+ // Equation numbers are written in as text rather than left to the CSS
133
+ // counter: Paged.js strips counter-increment while rewriting counters for
134
+ // its own pagination, which rendered every equation as "(0)".
135
+ body = numberEquations(body).html
136
+
132
137
  const lang = meta.language ?? 'en'
133
138
  const title = meta.title ?? 'Book'
134
139
  // Math CSS is emitted ahead of the design stylesheet, not inside it.
@@ -238,7 +243,7 @@ export const DEFAULT_POLYFILL_URL =
238
243
  * book PDF without a custom foundation stylesheet. Foundations override by
239
244
  * passing `options.stylesheet`.
240
245
  */
241
- import { MATH_CSS } from './math-css.js'
246
+ import { MATH_CSS, numberEquations } from './math-css.js'
242
247
 
243
248
  export const DEFAULT_STYLESHEET = `
244
249
  /* Hidden metadata block — the DOM carries it for CSS string-set / string() */
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { parseStyledString } from './parseStyledString.js'
11
11
  import TextRun from './TextRun.jsx'
12
+ import LinkPart from './LinkPart.jsx'
12
13
 
13
14
  function Heading({ level, data, children, ...props }) {
14
15
  const Tag = `h${level}`
@@ -20,22 +21,15 @@ function Heading({ level, data, children, ...props }) {
20
21
  <Tag data-type="paragraph" data-heading={`HEADING_${level}`} {...props}>
21
22
  {parts.map((part, i) =>
22
23
  part.type === 'link' ? (
23
- <a
24
- key={i}
25
- data-type="externalHyperlink"
26
- data-link={part.href}
27
- href={part.href}
28
- >
29
- <span data-type="text" data-style="Hyperlink">
30
- {part.content}
31
- </span>
32
- </a>
24
+ <LinkPart key={i} part={part} />
33
25
  ) : (
34
26
  <TextRun
35
27
  key={i}
36
28
  bold={part.bold}
37
29
  italics={part.italics}
38
30
  underline={!!part.underline}
31
+ subscript={part.subscript}
32
+ superscript={part.superscript}
39
33
  >
40
34
  {part.content}
41
35
  </TextRun>
package/src/docx/Link.jsx CHANGED
@@ -33,8 +33,15 @@ export default function Link({ data, ...props }) {
33
33
  )
34
34
  }
35
35
 
36
+ // The IR's anchor is a BARE bookmark name — `data-bookmark="x"` becomes
37
+ // `<w:bookmarkStart w:name="x"/>` — so a documented `href: '#section-3'`
38
+ // has to lose its '#' or `w:anchor="#section-3"` never matches
39
+ // `w:name="section-3"` and Word resolves nothing. `href` keeps the
40
+ // fragment form, which is what the browser preview needs.
41
+ const anchor = href.startsWith('#') ? href.slice(1) : href
42
+
36
43
  return (
37
- <a data-type="internalHyperlink" data-anchor={href} href={href} {...props}>
44
+ <a data-type="internalHyperlink" data-anchor={anchor} href={href} {...props}>
38
45
  <span data-type="text" data-style="Hyperlink">
39
46
  {label || href}
40
47
  </span>
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Renders a `link` part from parseStyledString. INTERNAL helper — not in the
3
+ * barrel, same as parseStyledString itself.
4
+ *
5
+ * A link's label is one or more styled runs rather than a string, because a
6
+ * mark composes with an <a> from either side (`<strong><a>…</a></strong>`,
7
+ * `<a><strong>…</strong></a>`) and a label can mix runs (`x<sup>2</sup>`).
8
+ * Rendering each run through <TextRun> keeps the Hyperlink character style
9
+ * alongside the mark instead of trading one for the other.
10
+ *
11
+ * Not to be confused with the public <Link> builder, which is a different
12
+ * contract: it takes `{ label, href }` and adds target/rel. Both resolve an
13
+ * in-document anchor the same way (see below).
14
+ */
15
+ import TextRun from './TextRun.jsx'
16
+
17
+ /** `parts` is absent only for a hand-built part; fall back to the label. */
18
+ export function labelRuns(part) {
19
+ return part.parts?.length ? part.parts : [{ content: part.content }]
20
+ }
21
+
22
+ export default function LinkPart({ part }) {
23
+ const href = part.href || ''
24
+
25
+ // An href starting with '#' is an in-document anchor, not a destination.
26
+ // Emitting it as an external hyperlink produced a relationship to the
27
+ // literal '#id' and no jump at all. The IR's anchor is a BARE bookmark
28
+ // name — `data-bookmark="x"` becomes `<w:bookmarkStart w:name="x"/>` — so
29
+ // the '#' is dropped here rather than shipped into `w:anchor`.
30
+ const anchor = href.startsWith('#') ? href.slice(1) : null
31
+ const linkAttrs = anchor
32
+ ? { 'data-type': 'internalHyperlink', 'data-anchor': anchor }
33
+ : { 'data-type': 'externalHyperlink', 'data-link': href }
34
+
35
+ return (
36
+ <a {...linkAttrs} href={href}>
37
+ {labelRuns(part).map((run, i) => (
38
+ <TextRun
39
+ key={i}
40
+ style="Hyperlink"
41
+ bold={run.bold}
42
+ italics={run.italics}
43
+ underline={!!run.underline}
44
+ subscript={run.subscript}
45
+ superscript={run.superscript}
46
+ >
47
+ {run.content}
48
+ </TextRun>
49
+ ))}
50
+ </a>
51
+ )
52
+ }
@@ -2,8 +2,9 @@
2
2
  * Block-level text container with optional styled string parsing.
3
3
  *
4
4
  * - Without `data`: renders children directly with data-type="paragraph".
5
- * - With `data`: parses the HTML string for inline marks (<strong>, <em>,
6
- * <u>) and hyperlinks (<a href="...">) and renders as styled children.
5
+ * - With `data`: parses the HTML string for inline marks (<strong>, <em>, <u>,
6
+ * <sub>, <sup>) and hyperlinks (<a href="...">) and renders as styled
7
+ * children. A mark composing with a link applies to the link's label.
7
8
  *
8
9
  * The `as` prop changes the rendered HTML element (default: <p>).
9
10
  *
@@ -11,6 +12,7 @@
11
12
  */
12
13
  import { parseStyledString } from './parseStyledString.js'
13
14
  import TextRun from './TextRun.jsx'
15
+ import LinkPart from './LinkPart.jsx'
14
16
  import Math from './Math.jsx'
15
17
 
16
18
  /**
@@ -42,8 +44,9 @@ function paragraphPolishProps({ tabStops, indent, role }) {
42
44
  * Block-level text container with optional styled string parsing.
43
45
  *
44
46
  * - Without `data`: renders children directly with data-type="paragraph".
45
- * - With `data`: parses the HTML string for inline marks (<strong>, <em>,
46
- * <u>) and hyperlinks (<a href="...">) and renders as styled children.
47
+ * - With `data`: parses the HTML string for inline marks (<strong>, <em>, <u>,
48
+ * <sub>, <sup>) and hyperlinks (<a href="...">) and renders as styled
49
+ * children. A mark composing with a link applies to the link's label.
47
50
  *
48
51
  * @param {Object} props
49
52
  * @param {React.ElementType} [props.as='p'] - Element to render.
@@ -74,18 +77,7 @@ export default function Paragraph({
74
77
  <Tag data-type="paragraph" {...polish} {...props}>
75
78
  {parts.map((part, i) => {
76
79
  if (part.type === 'link') {
77
- return (
78
- <a
79
- key={i}
80
- data-type="externalHyperlink"
81
- data-link={part.href}
82
- href={part.href}
83
- >
84
- <span data-type="text" data-style="Hyperlink">
85
- {part.content}
86
- </span>
87
- </a>
88
- )
80
+ return <LinkPart key={i} part={part} />
89
81
  }
90
82
  if (part.type === 'math') {
91
83
  return (
@@ -103,6 +95,8 @@ export default function Paragraph({
103
95
  bold={part.bold}
104
96
  italics={part.italics}
105
97
  underline={!!part.underline}
98
+ subscript={part.subscript}
99
+ superscript={part.superscript}
106
100
  >
107
101
  {part.content}
108
102
  </TextRun>
@@ -43,6 +43,8 @@ function coerceDateChildren(children) {
43
43
  *
44
44
  * @param {Object} props
45
45
  * @param {boolean} [props.bold]
46
+ * @param {boolean} [props.subscript] - Render below the baseline (docx vertAlign).
47
+ * @param {boolean} [props.superscript] - Render above the baseline (docx vertAlign).
46
48
  * @param {boolean} [props.italics]
47
49
  * @param {boolean} [props.underline]
48
50
  * @param {string} [props.color] - Hex color (with or without '#') or a
@@ -67,6 +69,8 @@ export default function TextRun({
67
69
  smallCaps,
68
70
  allCaps,
69
71
  strike,
72
+ subscript,
73
+ superscript,
70
74
  style,
71
75
  role,
72
76
  ...props
@@ -88,6 +92,8 @@ export default function TextRun({
88
92
  if (smallCaps) dataProps['data-smallcaps'] = 'true'
89
93
  if (allCaps) dataProps['data-allcaps'] = 'true'
90
94
  if (strike) dataProps['data-strike'] = 'true'
95
+ if (subscript) dataProps['data-subscript'] = 'true'
96
+ if (superscript) dataProps['data-superscript'] = 'true'
91
97
  if (resolvedStyle) dataProps['data-style'] = resolvedStyle
92
98
 
93
99
  return (
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Parses a styled string with inline HTML marks (<strong>, <em>, <u>, <a>,
3
- * <span data-type="math">) into an array of text / link / math part
4
- * objects with style flags.
2
+ * Parses a styled string with inline HTML marks (<strong>/<b>, <em>/<i>, <u>,
3
+ * <sub>, <sup>, <a>, <span data-type="math">) into an array of text / link /
4
+ * math part objects with style flags.
5
5
  *
6
6
  * Ported from legacy report-sdk/src/utils.js:116-186, extended to support
7
7
  * <a href="..."> hyperlinks so paragraphs with auto-linked emails and URLs
@@ -10,8 +10,14 @@
10
10
  * than being walked as opaque DOM (which turns MathML into raw operator
11
11
  * text).
12
12
  *
13
+ * A `link` part carries `parts` — the styled runs making up its label — because
14
+ * a mark composes with an <a> from either side (`<strong><a>…</a></strong>` or
15
+ * `<a><strong>…</strong></a>`) and a label can mix runs (`x<sup>2</sup>`).
16
+ * `content` remains the flat plain-text label for callers that only want a
17
+ * string.
18
+ *
13
19
  * @param {string} inputString - HTML string with inline marks.
14
- * @returns {Array<{type: string, content?: string, bold?: boolean, italics?: boolean, underline?: object, href?: string, latex?: string, display?: boolean}>}
20
+ * @returns {Array<{type: string, content?: string, bold?: boolean, italics?: boolean, underline?: object, subscript?: boolean, superscript?: boolean, href?: string, parts?: Array<object>, latex?: string, display?: boolean}>}
15
21
  *
16
22
  * @example
17
23
  * parseStyledString('Hello <strong>World</strong>')
@@ -21,10 +27,15 @@
21
27
  * // ]
22
28
  *
23
29
  * @example
24
- * parseStyledString('Visit <a href="https://example.com">Example</a>')
30
+ * parseStyledString('Visit <a href="https://example.com"><strong>Example</strong></a>')
25
31
  * // => [
26
32
  * // { type: 'text', content: 'Visit ' },
27
- * // { type: 'link', content: 'Example', href: 'https://example.com' }
33
+ * // {
34
+ * // type: 'link',
35
+ * // content: 'Example',
36
+ * // href: 'https://example.com',
37
+ * // parts: [{ type: 'text', content: 'Example', bold: true }]
38
+ * // }
28
39
  * // ]
29
40
  *
30
41
  * @example
@@ -77,14 +88,40 @@ export function parseStyledString(inputString) {
77
88
  result.push(createTextPart(plainText, styles))
78
89
  }
79
90
 
80
- // Handle <a> tags as links
91
+ // Handle <a> tags as links.
92
+ //
93
+ // Marks compose with a link from both directions and both used to
94
+ // be lost here, because this branch returned without consulting
95
+ // the accumulated styles OR walking the body:
96
+ //
97
+ // <strong><a href=…>x</a></strong> outer mark -> dropped
98
+ // <a href=…><strong>x</strong></a> inner mark -> survived as a
99
+ // LITERAL '<strong>x</strong>'
100
+ // string, which React escapes
101
+ // and Word then shows as
102
+ // visible tag text.
103
+ //
104
+ // Parsing the body with the accumulated styles fixes both: an
105
+ // outer mark rides in via `styles`, an inner one is found by the
106
+ // recursion, and a link can now carry mixed runs (`x<sup>2</sup>`).
107
+ // `content` stays the flat plain-text label for callers that only
108
+ // want a string; `parts` carries the styled runs.
81
109
  if (tag === 'a' && attrs) {
82
110
  const href = readAttr(attrs, 'href')
83
111
  if (href) {
112
+ // Text runs only. A link wrapping a non-text atom (inline
113
+ // math) is exotic, and neither builder can place one inside
114
+ // an <a> today; dropping it yields a clean text label
115
+ // rather than the raw markup that used to leak through.
116
+ const parts = processSegments(innerText, styles).filter(
117
+ (p) => p.type === 'text',
118
+ )
119
+
84
120
  result.push({
85
121
  type: 'link',
86
- content: innerText,
122
+ content: parts.map((p) => p.content).join(''),
87
123
  href,
124
+ parts,
88
125
  })
89
126
  lastIndex = offset + match.length
90
127
  return
@@ -118,6 +155,8 @@ export function parseStyledString(inputString) {
118
155
  if (tag === 'strong' || tag === 'b') newStyles.bold = true
119
156
  if (tag === 'em' || tag === 'i') newStyles.italics = true
120
157
  if (tag === 'u') newStyles.underline = {}
158
+ if (tag === 'sub') newStyles.subscript = true
159
+ if (tag === 'sup') newStyles.superscript = true
121
160
 
122
161
  result = result.concat(processSegments(innerText, newStyles))
123
162
  lastIndex = offset + match.length
@@ -187,6 +187,8 @@ export const attributeMap = {
187
187
  'data-smallcaps': { path: ['smallCaps'], transform: asTrue },
188
188
  'data-allcaps': { path: ['allCaps'], transform: asTrue },
189
189
  'data-strike': { path: ['strike'], transform: asTrue },
190
+ 'data-subscript': { path: ['subScript'], transform: asTrue },
191
+ 'data-superscript': { path: ['superScript'], transform: asTrue },
190
192
 
191
193
  // Row-level: whether the row repeats as a header on each new page
192
194
  // when the table breaks. Presence-only — any truthy value counts.
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { parseStyledString } from '../docx/parseStyledString.js'
9
9
  import TextRun from './TextRun.jsx'
10
+ import LinkPart from './LinkPart.jsx'
10
11
 
11
12
  export default function Heading({ level = 1, id, data, children, ...props }) {
12
13
  const clamped = Math.min(Math.max(level, 1), 6)
@@ -20,14 +21,7 @@ export default function Heading({ level = 1, id, data, children, ...props }) {
20
21
  <Tag data-type="heading" data-level={clamped} {...idAttr} {...props}>
21
22
  {parts.map((part, i) =>
22
23
  part.type === 'link' ? (
23
- <a
24
- key={i}
25
- data-type="link"
26
- data-href={part.href}
27
- href={part.href}
28
- >
29
- <span data-type="text">{part.content}</span>
30
- </a>
24
+ <LinkPart key={i} part={part} />
31
25
  ) : (
32
26
  <TextRun
33
27
  key={i}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Renders a `link` part from parseStyledString. INTERNAL helper — not in the
3
+ * barrel, same as parseStyledString itself.
4
+ *
5
+ * A link's label is one or more styled runs rather than a string, because a
6
+ * mark composes with an <a> from either side (`<strong><a>…</a></strong>`,
7
+ * `<a><strong>…</strong></a>`) and a label can mix runs.
8
+ *
9
+ * The docx lane has its own copy: the two emit different `data-type` values
10
+ * and their TextRuns accept different marks (this one takes `code`; the docx
11
+ * one takes sub/superscript and a Hyperlink style). Parameterising one shared
12
+ * component over those differences would need more knobs than either version
13
+ * has lines.
14
+ *
15
+ * Unlike the docx copy, this one does NOT special-case an in-document anchor
16
+ * (`href="#id"`). That is deliberate: the typst adapter has no
17
+ * `internalHyperlink` case, so emitting one would drop the link entirely,
18
+ * whereas `#link("#id")` at least renders its label. Typst addresses
19
+ * in-document targets through labels rather than URL fragments, so wiring
20
+ * this up means teaching the adapter that concept first.
21
+ */
22
+ import TextRun from './TextRun.jsx'
23
+
24
+ /** `parts` is absent only for a hand-built part; fall back to the label. */
25
+ export function labelRuns(part) {
26
+ return part.parts?.length ? part.parts : [{ content: part.content }]
27
+ }
28
+
29
+ export default function LinkPart({ part }) {
30
+ return (
31
+ <a data-type="link" data-href={part.href} href={part.href}>
32
+ {labelRuns(part).map((run, i) => (
33
+ <TextRun
34
+ key={i}
35
+ bold={run.bold}
36
+ italics={run.italics}
37
+ underline={!!run.underline}
38
+ code={run.code}
39
+ >
40
+ {run.content}
41
+ </TextRun>
42
+ ))}
43
+ </a>
44
+ )
45
+ }
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import { parseStyledString } from '../docx/parseStyledString.js'
15
15
  import TextRun from './TextRun.jsx'
16
+ import LinkPart from './LinkPart.jsx'
16
17
  import Math from './Math.jsx'
17
18
 
18
19
  export default function Paragraph({ as: Tag = 'p', data, children, ...props }) {
@@ -23,16 +24,7 @@ export default function Paragraph({ as: Tag = 'p', data, children, ...props }) {
23
24
  <Tag data-type="paragraph" {...props}>
24
25
  {parts.map((part, i) => {
25
26
  if (part.type === 'link') {
26
- return (
27
- <a
28
- key={i}
29
- data-type="link"
30
- data-href={part.href}
31
- href={part.href}
32
- >
33
- <span data-type="text">{part.content}</span>
34
- </a>
35
- )
27
+ return <LinkPart key={i} part={part} />
36
28
  }
37
29
  if (part.type === 'math') {
38
30
  return (