@uniweb/press 0.4.4 → 0.4.6
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 +4 -2
- package/src/DocumentProvider.jsx +56 -18
- package/src/ThemeContext.js +237 -0
- package/src/adapters/docx.js +276 -11
- package/src/adapters/latex.js +18 -0
- package/src/adapters/typst.js +34 -2
- package/src/compileRegistrations.js +4 -4
- package/src/docx/BrandLogo.jsx +59 -0
- package/src/docx/Math.jsx +22 -0
- package/src/docx/PageChrome.jsx +109 -0
- package/src/docx/Paragraph.jsx +93 -17
- package/src/docx/Table.jsx +156 -21
- package/src/docx/TextRun.jsx +83 -4
- package/src/docx/index.js +14 -1
- package/src/docx/parseStyledString.js +59 -6
- package/src/docx/units.js +21 -0
- package/src/format/index.jsx +171 -0
- package/src/index.js +6 -0
- package/src/ir/attributes.js +145 -0
- package/src/styles/buildStylePack.js +190 -0
- package/src/typst/Math.jsx +23 -0
- package/src/typst/Paragraph.jsx +26 -12
- package/src/typst/Sequence.jsx +15 -0
- package/src/typst/index.js +1 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page-chrome builders — page header, page footer, and page-number
|
|
3
|
+
* field codes.
|
|
4
|
+
*
|
|
5
|
+
* Stage 4 of kb/framework/plans/press-professional-docx.md.
|
|
6
|
+
*
|
|
7
|
+
* The underlying registration plumbing already exists in Press:
|
|
8
|
+
* `useDocumentOutput(block, 'docx', body, { role: 'header' })` lifts
|
|
9
|
+
* a fragment into the docx Section's header; `role: 'footer'` does the
|
|
10
|
+
* same for the footer. These builders are ergonomic wrappers — they
|
|
11
|
+
* give foundations a JSX shape that matches how Word documents are
|
|
12
|
+
* usually authored (a tree with PageHeader / PageFooter elements at
|
|
13
|
+
* the top level), and route the registration internally.
|
|
14
|
+
*
|
|
15
|
+
* Usage in a section component:
|
|
16
|
+
*
|
|
17
|
+
* import {
|
|
18
|
+
* useDocumentOutput,
|
|
19
|
+
* useDocumentOutputAsHeader,
|
|
20
|
+
* useDocumentOutputAsFooter,
|
|
21
|
+
* } from '@uniweb/press'
|
|
22
|
+
* import {
|
|
23
|
+
* PageHeader, PageFooter, PageNumber, TotalPages,
|
|
24
|
+
* } from '@uniweb/press/docx'
|
|
25
|
+
*
|
|
26
|
+
* function Cover({ block }) {
|
|
27
|
+
* useDocumentOutput(block, 'docx', body)
|
|
28
|
+
* useDocumentOutputAsHeader(block, 'docx',
|
|
29
|
+
* <PageHeader>
|
|
30
|
+
* <Paragraph data-alignment="right">
|
|
31
|
+
* <Image src="logo.png" />
|
|
32
|
+
* </Paragraph>
|
|
33
|
+
* </PageHeader>
|
|
34
|
+
* )
|
|
35
|
+
* useDocumentOutputAsFooter(block, 'docx',
|
|
36
|
+
* <PageFooter>
|
|
37
|
+
* <Paragraph data-alignment="center">
|
|
38
|
+
* <TextRun>Page </TextRun><PageNumber />
|
|
39
|
+
* <TextRun> of </TextRun><TotalPages />
|
|
40
|
+
* </Paragraph>
|
|
41
|
+
* </PageFooter>
|
|
42
|
+
* )
|
|
43
|
+
* }
|
|
44
|
+
*
|
|
45
|
+
* The PageHeader / PageFooter components are layout-transparent: they
|
|
46
|
+
* render their children inside an HTML container that the IR walker
|
|
47
|
+
* treats as a normal sequence of paragraphs. The role-based routing
|
|
48
|
+
* happens at the registration layer (useDocumentOutputAsHeader / Footer),
|
|
49
|
+
* not via element identity. This lets foundations build complex headers
|
|
50
|
+
* (multi-row layouts, brand blocks) without the IR walker needing a
|
|
51
|
+
* special "page header" type.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Wrapper element for a page header. Renders its children with no
|
|
56
|
+
* additional structure — used as a documentation/identity hint at the
|
|
57
|
+
* top of a header tree, and to give foundations a single JSX node to
|
|
58
|
+
* pass to `useDocumentOutputAsHeader`.
|
|
59
|
+
*/
|
|
60
|
+
export function PageHeader({ children }) {
|
|
61
|
+
return <>{children}</>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Wrapper element for a page footer. Mirror of PageHeader.
|
|
66
|
+
*/
|
|
67
|
+
export function PageFooter({ children }) {
|
|
68
|
+
return <>{children}</>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The current page number, as a Word field code that updates on
|
|
73
|
+
* print/preview. Inline element — usable inside any TextRun-context
|
|
74
|
+
* (Paragraph children, table cells, etc.).
|
|
75
|
+
*
|
|
76
|
+
* The IR walker has special-cased '_currentPage' as text content
|
|
77
|
+
* since pre-Press days; we emit a TextRun-shaped span carrying
|
|
78
|
+
* exactly that sentinel so the existing pipeline turns it into a
|
|
79
|
+
* <w:fldSimple> / <w:fldChar> page reference.
|
|
80
|
+
*/
|
|
81
|
+
export function PageNumber() {
|
|
82
|
+
return <span data-type="text">_currentPage</span>
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The total number of pages. Companion to PageNumber for "Page X of Y"
|
|
87
|
+
* footer patterns.
|
|
88
|
+
*/
|
|
89
|
+
export function TotalPages() {
|
|
90
|
+
return <span data-type="text">_totalPages</span>
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Common page-size presets, in twips. Foundations pass these to
|
|
95
|
+
* `compile('docx', { pageSize: pageSizes.A4 })` to override Word's
|
|
96
|
+
* default. The orientation field is set separately via the
|
|
97
|
+
* `pageOrientation` compile option (or by spreading + overriding).
|
|
98
|
+
*
|
|
99
|
+
* ISO A-series and US presets are exact. Other sizes available
|
|
100
|
+
* directly through the docx library if needed.
|
|
101
|
+
*/
|
|
102
|
+
export const pageSizes = {
|
|
103
|
+
A4: { width: 11906, height: 16838 }, // 21.0 × 29.7 cm
|
|
104
|
+
A5: { width: 8419, height: 11906 }, // 14.8 × 21.0 cm
|
|
105
|
+
A3: { width: 16838, height: 23811 }, // 29.7 × 42.0 cm
|
|
106
|
+
LETTER: { width: 12240, height: 15840 }, // 8.5 × 11 in
|
|
107
|
+
LEGAL: { width: 12240, height: 20160 }, // 8.5 × 14 in
|
|
108
|
+
TABLOID: { width: 15840, height: 24480 }, // 11 × 17 in
|
|
109
|
+
}
|
package/src/docx/Paragraph.jsx
CHANGED
|
@@ -11,26 +11,93 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { parseStyledString } from './parseStyledString.js'
|
|
13
13
|
import TextRun from './TextRun.jsx'
|
|
14
|
+
import Math from './Math.jsx'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build the data-* attribute pairs for paragraph-level Stage-3 props.
|
|
18
|
+
* Returns a plain object that gets spread onto the rendered element.
|
|
19
|
+
*/
|
|
20
|
+
function paragraphPolishProps({ tabStops, indent, role }) {
|
|
21
|
+
const out = {}
|
|
22
|
+
if (Array.isArray(tabStops) && tabStops.length) {
|
|
23
|
+
out['data-tab-stops'] = JSON.stringify(tabStops)
|
|
24
|
+
}
|
|
25
|
+
if (indent && typeof indent === 'object') {
|
|
26
|
+
// Lower-cased data-* keys to match the IR attribute map.
|
|
27
|
+
if (indent.left != null) out['data-indent-left'] = indent.left
|
|
28
|
+
if (indent.right != null) out['data-indent-right'] = indent.right
|
|
29
|
+
if (indent.firstLine != null) out['data-indent-firstline'] = indent.firstLine
|
|
30
|
+
if (indent.hanging != null) out['data-indent-hanging'] = indent.hanging
|
|
31
|
+
}
|
|
32
|
+
// Stage 6.0: <Paragraph role="Title"> attaches a named OOXML
|
|
33
|
+
// paragraph style. Resolution to actual font/size/color happens
|
|
34
|
+
// through theme.typography at compile time.
|
|
35
|
+
if (typeof role === 'string' && role.length) {
|
|
36
|
+
out['data-paragraph-style'] = role
|
|
37
|
+
}
|
|
38
|
+
return out
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Block-level text container with optional styled string parsing.
|
|
43
|
+
*
|
|
44
|
+
* - 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
|
+
*
|
|
48
|
+
* @param {Object} props
|
|
49
|
+
* @param {React.ElementType} [props.as='p'] - Element to render.
|
|
50
|
+
* @param {string} [props.data] - HTML string with inline marks.
|
|
51
|
+
* @param {Array<{position:number, type?:string, leader?:string}>} [props.tabStops]
|
|
52
|
+
* Stage 3: paragraph-level tab stops. Position is in twips; type defaults
|
|
53
|
+
* to 'left'; leader is 'none' (default), 'dot', 'hyphen', 'underscore',
|
|
54
|
+
* 'middleDot'. Combine with `<Tab/>` (or `{'\t'}`) inside the paragraph
|
|
55
|
+
* children to align text on the stops.
|
|
56
|
+
* @param {{left?:number,right?:number,firstLine?:number,hanging?:number}} [props.indent]
|
|
57
|
+
* Stage 3: paragraph indentation, in twips.
|
|
58
|
+
*/
|
|
59
|
+
export default function Paragraph({
|
|
60
|
+
as: Tag = 'p',
|
|
61
|
+
data,
|
|
62
|
+
tabStops,
|
|
63
|
+
indent,
|
|
64
|
+
role,
|
|
65
|
+
children,
|
|
66
|
+
...props
|
|
67
|
+
}) {
|
|
68
|
+
const polish = paragraphPolishProps({ tabStops, indent, role })
|
|
14
69
|
|
|
15
|
-
export default function Paragraph({ as: Tag = 'p', data, children, ...props }) {
|
|
16
70
|
if (data) {
|
|
17
71
|
const parts = parseStyledString(data)
|
|
18
72
|
|
|
19
73
|
return (
|
|
20
|
-
<Tag data-type="paragraph" {...props}>
|
|
21
|
-
{parts.map((part, i) =>
|
|
22
|
-
part.type === 'link'
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
74
|
+
<Tag data-type="paragraph" {...polish} {...props}>
|
|
75
|
+
{parts.map((part, i) => {
|
|
76
|
+
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
|
+
)
|
|
89
|
+
}
|
|
90
|
+
if (part.type === 'math') {
|
|
91
|
+
return (
|
|
92
|
+
<Math
|
|
93
|
+
key={i}
|
|
94
|
+
latex={part.latex}
|
|
95
|
+
display={part.display}
|
|
96
|
+
id={part.id}
|
|
97
|
+
/>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
return (
|
|
34
101
|
<TextRun
|
|
35
102
|
key={i}
|
|
36
103
|
bold={part.bold}
|
|
@@ -40,18 +107,27 @@ export default function Paragraph({ as: Tag = 'p', data, children, ...props }) {
|
|
|
40
107
|
{part.content}
|
|
41
108
|
</TextRun>
|
|
42
109
|
)
|
|
43
|
-
)}
|
|
110
|
+
})}
|
|
44
111
|
</Tag>
|
|
45
112
|
)
|
|
46
113
|
}
|
|
47
114
|
|
|
48
115
|
return (
|
|
49
|
-
<Tag data-type="paragraph" {...props}>
|
|
116
|
+
<Tag data-type="paragraph" {...polish} {...props}>
|
|
50
117
|
{children}
|
|
51
118
|
</Tag>
|
|
52
119
|
)
|
|
53
120
|
}
|
|
54
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Inline tab marker. Emits a docx Tab element that interacts with the
|
|
124
|
+
* parent paragraph's tabStops. JSX-friendly alternative to writing
|
|
125
|
+
* `{'\t'}` literally (which React's whitespace handling can swallow).
|
|
126
|
+
*/
|
|
127
|
+
export function Tab(props) {
|
|
128
|
+
return <span data-type="tab" {...props} />
|
|
129
|
+
}
|
|
130
|
+
|
|
55
131
|
/**
|
|
56
132
|
* Render an array of paragraph strings. Convenience wrapper used by
|
|
57
133
|
* the default Section component pattern.
|
package/src/docx/Table.jsx
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Table, Tr, Td — ergonomic wrappers for Press's table vocabulary.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* with data-type="table"/"tableRow"/"tableCell" attributes plus width,
|
|
6
|
-
* margin, and border data attributes per cell. These components ship
|
|
7
|
-
* sensible defaults and a shared column-widths channel, so the common
|
|
8
|
-
* case reduces to:
|
|
4
|
+
* Common case (percent column widths, soft borders):
|
|
9
5
|
*
|
|
10
6
|
* <Table widths={[15, 60, 25]}>
|
|
11
7
|
* <Tr header>
|
|
@@ -20,6 +16,20 @@
|
|
|
20
16
|
* </Tr>
|
|
21
17
|
* </Table>
|
|
22
18
|
*
|
|
19
|
+
* Stage 1 of press-professional-docx adds:
|
|
20
|
+
*
|
|
21
|
+
* - <Table columnWidths={[cm(15), cm(4), cm(1.5), cm(4)]} layout="fixed">
|
|
22
|
+
* fixed-width columns specified in twips (use the unit helpers).
|
|
23
|
+
* - <Table width={{ size: 100, type: 'pct' }}>
|
|
24
|
+
* whole-table width.
|
|
25
|
+
* - <Table borders={{ top: { style: 'single', size: 4, color: 'cccccc' }, … }}>
|
|
26
|
+
* table-default borders. Per-cell borders still win.
|
|
27
|
+
* - <Td shading="4775b2"> or <Td shading={{ fill: '4775b2', type: 'clear' }}>
|
|
28
|
+
* cell background color.
|
|
29
|
+
* - <Td valign="center"> top | center | bottom.
|
|
30
|
+
* - <Tr header> now also marks the row as a docx tableHeader, so it
|
|
31
|
+
* repeats at the top of each new page when the table breaks.
|
|
32
|
+
*
|
|
23
33
|
* Every data-* attribute set by default can be overridden by passing the
|
|
24
34
|
* same attribute on the Td — explicit spreads win. String children are
|
|
25
35
|
* wrapped in a Press <Paragraph> automatically; complex cells (multiple
|
|
@@ -38,6 +48,26 @@ import {
|
|
|
38
48
|
} from 'react'
|
|
39
49
|
import Paragraph from './Paragraph.jsx'
|
|
40
50
|
import TextRun from './TextRun.jsx'
|
|
51
|
+
import { useDocumentTheme, resolveThemeColor } from '../ThemeContext.js'
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Run a borders object through theme color resolution. Each side's
|
|
55
|
+
* `color` key may be a hex literal or a theme key like 'softBorder'.
|
|
56
|
+
*/
|
|
57
|
+
function resolveBordersTheme(borders, theme) {
|
|
58
|
+
if (!borders) return borders
|
|
59
|
+
const out = {}
|
|
60
|
+
for (const [side, sideProps] of Object.entries(borders)) {
|
|
61
|
+
if (!sideProps) continue
|
|
62
|
+
out[side] = {
|
|
63
|
+
...sideProps,
|
|
64
|
+
...(sideProps.color
|
|
65
|
+
? { color: resolveThemeColor(sideProps.color, theme) }
|
|
66
|
+
: {}),
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return out
|
|
70
|
+
}
|
|
41
71
|
|
|
42
72
|
const TableCtx = createContext({ widths: null, borderColor: 'cccccc' })
|
|
43
73
|
|
|
@@ -45,20 +75,83 @@ const TableCtx = createContext({ widths: null, borderColor: 'cccccc' })
|
|
|
45
75
|
* Table — the outer wrapper.
|
|
46
76
|
*
|
|
47
77
|
* @param {Object} props
|
|
48
|
-
* @param {number[]} [props.widths] - Column widths in percent
|
|
49
|
-
* width by column index.
|
|
50
|
-
*
|
|
78
|
+
* @param {number[]} [props.widths] - Column widths in percent. Each Td reads
|
|
79
|
+
* its width by column index. Cell-level widths are emitted via
|
|
80
|
+
* `data-width-size`/`data-width-type=pct`.
|
|
81
|
+
* @param {number[]} [props.columnWidths] - Column widths in twips. Sets
|
|
82
|
+
* docx `columnWidths` for layout-aware sizing. Use the unit helpers
|
|
83
|
+
* (cm, mm, inch, pt) to express them naturally. When set, `layout`
|
|
84
|
+
* defaults to 'fixed' so Word doesn't redistribute columns.
|
|
85
|
+
* @param {'fixed'|'autofit'} [props.layout] - Table layout type. 'fixed'
|
|
86
|
+
* honors columnWidths exactly; 'autofit' lets Word redistribute to
|
|
87
|
+
* fit content. Defaults to 'fixed' when columnWidths is provided.
|
|
88
|
+
* @param {{size: number, type?: string}} [props.width] - Whole-table width.
|
|
89
|
+
* { size, type } where type is 'pct' (percent), 'dxa' (twips), or 'auto'.
|
|
90
|
+
* @param {Object} [props.borders] - Table-default borders. Same shape as
|
|
91
|
+
* per-cell borders: { top, bottom, left, right, insideHorizontal,
|
|
92
|
+
* insideVertical } each with { style, size, color }.
|
|
93
|
+
* @param {string} [props.borderColor='cccccc'] - Hex (no #) for cell-level
|
|
94
|
+
* default borders. Used by Td when no per-cell borders are set.
|
|
51
95
|
*/
|
|
52
96
|
export function Table({
|
|
53
97
|
widths,
|
|
98
|
+
columnWidths,
|
|
99
|
+
layout,
|
|
100
|
+
width,
|
|
101
|
+
borders,
|
|
54
102
|
borderColor = 'cccccc',
|
|
55
103
|
className,
|
|
56
104
|
children,
|
|
57
105
|
...props
|
|
58
106
|
}) {
|
|
107
|
+
const theme = useDocumentTheme()
|
|
108
|
+
const tableAttrs = {}
|
|
109
|
+
|
|
110
|
+
if (columnWidths && columnWidths.length) {
|
|
111
|
+
tableAttrs['data-table-column-widths'] = columnWidths.join(',')
|
|
112
|
+
// Default to fixed when the foundation gave us widths — Word
|
|
113
|
+
// otherwise redistributes them. Honor an explicit `layout` prop.
|
|
114
|
+
tableAttrs['data-table-layout'] = layout || 'fixed'
|
|
115
|
+
} else if (layout) {
|
|
116
|
+
tableAttrs['data-table-layout'] = layout
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (width) {
|
|
120
|
+
if (width.size != null) tableAttrs['data-table-width-size'] = width.size
|
|
121
|
+
tableAttrs['data-table-width-type'] = width.type ?? 'pct'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const resolvedBorders = resolveBordersTheme(borders, theme)
|
|
125
|
+
if (resolvedBorders) {
|
|
126
|
+
for (const [side, sideProps] of Object.entries(resolvedBorders)) {
|
|
127
|
+
if (!sideProps) continue
|
|
128
|
+
// The IR map uses `insideh`/`insidev` (single token) to keep
|
|
129
|
+
// attribute names parser-friendly. Translate from camelCase.
|
|
130
|
+
const sideKey =
|
|
131
|
+
side === 'insideHorizontal'
|
|
132
|
+
? 'insideh'
|
|
133
|
+
: side === 'insideVertical'
|
|
134
|
+
? 'insidev'
|
|
135
|
+
: side
|
|
136
|
+
if (sideProps.style)
|
|
137
|
+
tableAttrs[`data-table-borders-${sideKey}-style`] = sideProps.style
|
|
138
|
+
if (sideProps.size != null)
|
|
139
|
+
tableAttrs[`data-table-borders-${sideKey}-size`] = sideProps.size
|
|
140
|
+
if (sideProps.color)
|
|
141
|
+
tableAttrs[`data-table-borders-${sideKey}-color`] = sideProps.color
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Resolve borderColor against the theme too — it's the per-cell
|
|
146
|
+
// default border color used when a Td doesn't override.
|
|
147
|
+
const resolvedBorderColor =
|
|
148
|
+
resolveThemeColor(borderColor, theme) ?? borderColor
|
|
149
|
+
|
|
59
150
|
return (
|
|
60
|
-
<TableCtx.Provider
|
|
61
|
-
|
|
151
|
+
<TableCtx.Provider
|
|
152
|
+
value={{ widths, borderColor: resolvedBorderColor, theme }}
|
|
153
|
+
>
|
|
154
|
+
<div data-type="table" className={className} {...tableAttrs} {...props}>
|
|
62
155
|
{children}
|
|
63
156
|
</div>
|
|
64
157
|
</TableCtx.Provider>
|
|
@@ -72,20 +165,34 @@ export function Table({
|
|
|
72
165
|
* cell can look up its width from the Table context, and to mark cells
|
|
73
166
|
* in a `header` row for the default emphasis + heavier bottom border.
|
|
74
167
|
*
|
|
168
|
+
* Column-index assignment is span-aware: a <Td colSpan={3}> takes
|
|
169
|
+
* three column slots, so the next cell is at index 3, not 1. Without
|
|
170
|
+
* this, a row with a colSpan'd cell followed by a per-column-width
|
|
171
|
+
* cell would pull the wrong width from `widths={…}`.
|
|
172
|
+
*
|
|
75
173
|
* @param {Object} props
|
|
76
|
-
* @param {boolean} [props.header=false] - Bolds text
|
|
174
|
+
* @param {boolean} [props.header=false] - Bolds text, thickens the bottom
|
|
175
|
+
* border, and marks the row as a docx tableHeader (it repeats at the
|
|
176
|
+
* top of each new page when the table breaks across pages).
|
|
77
177
|
*/
|
|
78
178
|
export function Tr({ header = false, className, children, ...props }) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
179
|
+
let nextCol = 0
|
|
180
|
+
const cells = Children.toArray(children).map((child) => {
|
|
181
|
+
if (!isValidElement(child)) return child
|
|
182
|
+
const col = child.props._col ?? nextCol
|
|
183
|
+
const span =
|
|
184
|
+
typeof child.props.colSpan === 'number' && child.props.colSpan > 1
|
|
185
|
+
? child.props.colSpan
|
|
186
|
+
: 1
|
|
187
|
+
nextCol = col + span
|
|
188
|
+
return cloneElement(child, {
|
|
189
|
+
_col: col,
|
|
190
|
+
_header: child.props._header ?? header,
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
const rowAttrs = header ? { 'data-row-header': '' } : {}
|
|
87
194
|
return (
|
|
88
|
-
<div data-type="tableRow" className={className} {...props}>
|
|
195
|
+
<div data-type="tableRow" className={className} {...rowAttrs} {...props}>
|
|
89
196
|
{cells}
|
|
90
197
|
</div>
|
|
91
198
|
)
|
|
@@ -108,6 +215,12 @@ const CELL_PAD = { top: 80, bottom: 80, left: 120, right: 120 }
|
|
|
108
215
|
* @param {number} [props.width] - Column width in percent (overrides Table.widths).
|
|
109
216
|
* @param {boolean} [props.emphasis=false] - Force bold regardless of header state.
|
|
110
217
|
* @param {'single'|'double'|'none'|string} [props.borderBottom] - Bottom border style.
|
|
218
|
+
* @param {string|{fill: string, type?: string, color?: string}} [props.shading] -
|
|
219
|
+
* Cell background fill. String shorthand is the hex color (no '#'),
|
|
220
|
+
* defaulting to a solid fill. Object form lets you set the OOXML
|
|
221
|
+
* shading type ('clear' default = plain fill, 'pct25' / 'horzStripe' / …
|
|
222
|
+
* for patterns).
|
|
223
|
+
* @param {'top'|'center'|'bottom'} [props.valign] - Vertical alignment of cell content.
|
|
111
224
|
*/
|
|
112
225
|
export function Td({
|
|
113
226
|
_col = 0,
|
|
@@ -115,12 +228,16 @@ export function Td({
|
|
|
115
228
|
width,
|
|
116
229
|
emphasis = false,
|
|
117
230
|
borderBottom,
|
|
231
|
+
shading,
|
|
232
|
+
valign,
|
|
233
|
+
colSpan,
|
|
234
|
+
rowSpan,
|
|
118
235
|
className,
|
|
119
236
|
style,
|
|
120
237
|
children,
|
|
121
238
|
...rest
|
|
122
239
|
}) {
|
|
123
|
-
const { widths, borderColor } = useContext(TableCtx)
|
|
240
|
+
const { widths, borderColor, theme } = useContext(TableCtx)
|
|
124
241
|
const colWidth = width ?? widths?.[_col]
|
|
125
242
|
|
|
126
243
|
const defaults = {
|
|
@@ -140,6 +257,24 @@ export function Td({
|
|
|
140
257
|
defaults['data-width-size'] = colWidth
|
|
141
258
|
defaults['data-width-type'] = 'pct'
|
|
142
259
|
}
|
|
260
|
+
if (shading) {
|
|
261
|
+
const s =
|
|
262
|
+
typeof shading === 'string' ? { fill: shading } : shading
|
|
263
|
+
const resolvedFill = resolveThemeColor(s.fill, theme)
|
|
264
|
+
const resolvedColor = resolveThemeColor(s.color, theme)
|
|
265
|
+
if (resolvedFill) defaults['data-shading-fill'] = resolvedFill
|
|
266
|
+
if (s.type) defaults['data-shading-type'] = s.type
|
|
267
|
+
if (resolvedColor) defaults['data-shading-color'] = resolvedColor
|
|
268
|
+
}
|
|
269
|
+
if (valign) {
|
|
270
|
+
defaults['data-valign'] = valign
|
|
271
|
+
}
|
|
272
|
+
if (typeof colSpan === 'number' && colSpan > 1) {
|
|
273
|
+
defaults['data-grid-span'] = colSpan
|
|
274
|
+
}
|
|
275
|
+
if (typeof rowSpan === 'number' && rowSpan > 1) {
|
|
276
|
+
defaults['data-row-span'] = rowSpan
|
|
277
|
+
}
|
|
143
278
|
|
|
144
279
|
const flexStyle =
|
|
145
280
|
colWidth != null
|
package/src/docx/TextRun.jsx
CHANGED
|
@@ -1,19 +1,98 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useDocumentTheme,
|
|
3
|
+
resolveThemeColor,
|
|
4
|
+
resolveThemeFont,
|
|
5
|
+
} from '../ThemeContext.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Stage 6.1 safety net. If a Date object reaches a TextRun as a
|
|
9
|
+
* child, coerce it to ISO YYYY-MM-DD before React's default
|
|
10
|
+
* stringification produces "Sat Feb 28 2026 19:00:00 GMT-0500 …".
|
|
11
|
+
*
|
|
12
|
+
* The right way to render a date is via `<DateText value={…} format="…"/>`
|
|
13
|
+
* from @uniweb/press/format, which gives the foundation control over
|
|
14
|
+
* locale and format. This safety net is the floor: a careless
|
|
15
|
+
* `<TextRun>{date}</TextRun>` produces a readable date instead of a
|
|
16
|
+
* garbage timezone string — and avoids the React 19 "Objects are not
|
|
17
|
+
* valid as a React child (found: Invalid Date)" runtime error.
|
|
18
|
+
*
|
|
19
|
+
* Walks one level of direct children. We can't use React.Children.map
|
|
20
|
+
* here because it rejects Date objects upfront (only plain objects
|
|
21
|
+
* are special-cased; Date counts as "object").
|
|
22
|
+
*/
|
|
23
|
+
function coerceDate(child) {
|
|
24
|
+
if (!(child instanceof Date)) return child
|
|
25
|
+
if (Number.isNaN(child.getTime())) return ''
|
|
26
|
+
const y = child.getUTCFullYear()
|
|
27
|
+
const m = String(child.getUTCMonth() + 1).padStart(2, '0')
|
|
28
|
+
const d = String(child.getUTCDate()).padStart(2, '0')
|
|
29
|
+
return `${y}-${m}-${d}`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function coerceDateChildren(children) {
|
|
33
|
+
if (children == null) return children
|
|
34
|
+
if (Array.isArray(children)) return children.map(coerceDate)
|
|
35
|
+
return coerceDate(children)
|
|
36
|
+
}
|
|
37
|
+
|
|
1
38
|
/**
|
|
2
39
|
* Inline text span. Renders <span data-type="text"> with optional
|
|
3
|
-
* bold/italic/underline via data attributes.
|
|
40
|
+
* bold/italic/underline + color/size/font via data attributes.
|
|
4
41
|
*
|
|
5
42
|
* Maps to the `text` IR node type → docx TextRun.
|
|
43
|
+
*
|
|
44
|
+
* @param {Object} props
|
|
45
|
+
* @param {boolean} [props.bold]
|
|
46
|
+
* @param {boolean} [props.italics]
|
|
47
|
+
* @param {boolean} [props.underline]
|
|
48
|
+
* @param {string} [props.color] - Hex color (with or without '#') or a
|
|
49
|
+
* theme key ('accent', 'body', 'muted', 'softBorder'). Theme keys
|
|
50
|
+
* resolve via the active <DocumentProvider theme={…}>; literals
|
|
51
|
+
* pass through after stripping any leading '#'.
|
|
52
|
+
* @param {number} [props.size] - Font size in half-points (so 28pt = 56).
|
|
53
|
+
* Use the convertPointsToHalfPoints helper to keep the doubling
|
|
54
|
+
* intent visible in foundation code.
|
|
55
|
+
* @param {string} [props.font] - Font family name (e.g. 'Calibri') or a
|
|
56
|
+
* theme key ('body', 'heading', 'mono').
|
|
57
|
+
* @param {string} [props.style] - Named character/paragraph style.
|
|
6
58
|
*/
|
|
7
|
-
export default function TextRun({
|
|
59
|
+
export default function TextRun({
|
|
60
|
+
children,
|
|
61
|
+
bold,
|
|
62
|
+
italics,
|
|
63
|
+
underline,
|
|
64
|
+
color,
|
|
65
|
+
size,
|
|
66
|
+
font,
|
|
67
|
+
smallCaps,
|
|
68
|
+
allCaps,
|
|
69
|
+
strike,
|
|
70
|
+
style,
|
|
71
|
+
role,
|
|
72
|
+
...props
|
|
73
|
+
}) {
|
|
74
|
+
const theme = useDocumentTheme()
|
|
75
|
+
const resolvedColor = resolveThemeColor(color, theme)
|
|
76
|
+
const resolvedFont = resolveThemeFont(font, theme)
|
|
77
|
+
// Stage 6.0: `role="Label"` references a named OOXML character
|
|
78
|
+
// style. Maps to `data-style` (the existing run-style channel).
|
|
79
|
+
// Inline overrides (color, size, bold, …) win over the style.
|
|
80
|
+
const resolvedStyle = style ?? role
|
|
8
81
|
const dataProps = { 'data-type': 'text' }
|
|
9
82
|
if (bold) dataProps['data-bold'] = 'true'
|
|
10
83
|
if (italics) dataProps['data-italics'] = 'true'
|
|
11
84
|
if (underline) dataProps['data-underline'] = 'true'
|
|
12
|
-
if (
|
|
85
|
+
if (resolvedColor) dataProps['data-color'] = resolvedColor
|
|
86
|
+
if (size != null) dataProps['data-size'] = size
|
|
87
|
+
if (resolvedFont) dataProps['data-font'] = resolvedFont
|
|
88
|
+
if (smallCaps) dataProps['data-smallcaps'] = 'true'
|
|
89
|
+
if (allCaps) dataProps['data-allcaps'] = 'true'
|
|
90
|
+
if (strike) dataProps['data-strike'] = 'true'
|
|
91
|
+
if (resolvedStyle) dataProps['data-style'] = resolvedStyle
|
|
13
92
|
|
|
14
93
|
return (
|
|
15
94
|
<span {...dataProps} {...props}>
|
|
16
|
-
{children}
|
|
95
|
+
{coerceDateChildren(children)}
|
|
17
96
|
</span>
|
|
18
97
|
)
|
|
19
98
|
}
|
package/src/docx/index.js
CHANGED
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
* '@uniweb/press/docx' does not pull in the ~3.4 MB docx library.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
export { default as Paragraph, Paragraphs } from './Paragraph.jsx'
|
|
14
|
+
export { default as Paragraph, Paragraphs, Tab } from './Paragraph.jsx'
|
|
15
15
|
export { default as TextRun } from './TextRun.jsx'
|
|
16
16
|
export { H1, H2, H3, H4 } from './Headings.jsx'
|
|
17
17
|
export { default as Image, Images } from './Image.jsx'
|
|
18
|
+
export { default as BrandLogo } from './BrandLogo.jsx'
|
|
19
|
+
export { default as Math } from './Math.jsx'
|
|
18
20
|
export { default as Caption } from './Caption.jsx'
|
|
19
21
|
export { default as Figure } from './Figure.jsx'
|
|
20
22
|
export { Table, Tr, Td } from './Table.jsx'
|
|
@@ -24,10 +26,21 @@ export { BulletList, NumberedList } from './Lists.jsx'
|
|
|
24
26
|
export { default as TableOfContents } from './TableOfContents.jsx'
|
|
25
27
|
export { default as FootnoteReference } from './FootnoteReference.jsx'
|
|
26
28
|
export { default as WebOnly } from './WebOnly.jsx'
|
|
29
|
+
export {
|
|
30
|
+
PageHeader,
|
|
31
|
+
PageFooter,
|
|
32
|
+
PageNumber,
|
|
33
|
+
TotalPages,
|
|
34
|
+
pageSizes,
|
|
35
|
+
} from './PageChrome.jsx'
|
|
27
36
|
|
|
28
37
|
export {
|
|
29
38
|
convertMillimetersToTwip,
|
|
30
39
|
convertCentimetersToTwip,
|
|
31
40
|
convertInchesToTwip,
|
|
32
41
|
convertPointsToHalfPoints,
|
|
42
|
+
cm,
|
|
43
|
+
mm,
|
|
44
|
+
inch,
|
|
45
|
+
pt,
|
|
33
46
|
} from './units.js'
|