@uniweb/press 0.4.14 → 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 +1 -1
- package/src/adapters/docx.js +4 -0
- package/src/docx/Headings.jsx +4 -10
- package/src/docx/Link.jsx +8 -1
- package/src/docx/LinkPart.jsx +52 -0
- package/src/docx/Paragraph.jsx +10 -16
- package/src/docx/TextRun.jsx +6 -0
- package/src/docx/parseStyledString.js +47 -8
- package/src/ir/attributes.js +2 -0
- package/src/typst/Heading.jsx +2 -8
- package/src/typst/LinkPart.jsx +45 -0
- package/src/typst/Paragraph.jsx +2 -10
package/package.json
CHANGED
package/src/adapters/docx.js
CHANGED
|
@@ -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
|
package/src/docx/Headings.jsx
CHANGED
|
@@ -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
|
-
<
|
|
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={
|
|
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
|
+
}
|
package/src/docx/Paragraph.jsx
CHANGED
|
@@ -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
|
-
* <
|
|
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
|
-
* <
|
|
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>
|
package/src/docx/TextRun.jsx
CHANGED
|
@@ -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>,
|
|
3
|
-
* <span data-type="math">) into an array of text / link /
|
|
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
|
-
* // {
|
|
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:
|
|
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
|
package/src/ir/attributes.js
CHANGED
|
@@ -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.
|
package/src/typst/Heading.jsx
CHANGED
|
@@ -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
|
-
<
|
|
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
|
+
}
|
package/src/typst/Paragraph.jsx
CHANGED
|
@@ -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 (
|