@uniweb/press 0.4.3 → 0.4.5
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 +273 -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 +135 -0
- package/src/latex/index.js +5 -0
- package/src/latex/insets.js +83 -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,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synthesize OOXML style definitions from a Press theme.
|
|
3
|
+
*
|
|
4
|
+
* Stage 6.0 of kb/framework/plans/press-professional-docx.md.
|
|
5
|
+
*
|
|
6
|
+
* The theme.typography registry is the foundation-facing API:
|
|
7
|
+
*
|
|
8
|
+
* theme.typography.Title = {
|
|
9
|
+
* font: 'heading', // theme key or literal family
|
|
10
|
+
* size: 56, // half-points (28pt)
|
|
11
|
+
* bold: true,
|
|
12
|
+
* color: 'accent', // theme key or hex
|
|
13
|
+
* paragraph: { spacing: { after: 240 } },
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* docx@9.x is opinionated about where named styles live. It ships
|
|
17
|
+
* built-in styles for Title, Heading1-6, Hyperlink, Strong, and
|
|
18
|
+
* ListParagraph; entries with those ids in the custom `paragraphStyles`
|
|
19
|
+
* array are ignored in favor of the built-ins. The library exposes
|
|
20
|
+
* `default.{title, heading1, …, hyperlink, strong, listParagraph}`
|
|
21
|
+
* slots specifically for overriding the built-ins. We route role
|
|
22
|
+
* specs to the right slot:
|
|
23
|
+
*
|
|
24
|
+
* - 'Body' / 'Normal' -> default.document (doc-wide rPrDefault/pPrDefault)
|
|
25
|
+
* - 'Title' -> default.title
|
|
26
|
+
* - 'Heading1'..'Heading6' -> default.heading1..6
|
|
27
|
+
* - 'Hyperlink' -> default.hyperlink
|
|
28
|
+
* - 'Strong' -> default.strong
|
|
29
|
+
* - 'ListParagraph' -> default.listParagraph
|
|
30
|
+
* - everything else, kind=paragraph -> custom paragraphStyles[]
|
|
31
|
+
* - everything else, kind=character -> custom characterStyles[]
|
|
32
|
+
*
|
|
33
|
+
* Foundations may also pass `paragraphStyles` / `characterStyles`
|
|
34
|
+
* directly through compile options; those merge on top of the
|
|
35
|
+
* synthesized entries (caller wins on style id conflict).
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { resolveThemeColor, resolveThemeFont, DEFAULT_THEME } from '../ThemeContext.js'
|
|
39
|
+
|
|
40
|
+
// Roles that map to docx's named built-in slots in `default.*`.
|
|
41
|
+
const BUILT_IN_DEFAULT_SLOTS = {
|
|
42
|
+
Body: 'document',
|
|
43
|
+
Normal: 'document',
|
|
44
|
+
Title: 'title',
|
|
45
|
+
Heading1: 'heading1',
|
|
46
|
+
Heading2: 'heading2',
|
|
47
|
+
Heading3: 'heading3',
|
|
48
|
+
Heading4: 'heading4',
|
|
49
|
+
Heading5: 'heading5',
|
|
50
|
+
Heading6: 'heading6',
|
|
51
|
+
Hyperlink: 'hyperlink',
|
|
52
|
+
Strong: 'strong',
|
|
53
|
+
ListParagraph: 'listParagraph',
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolve a single typography-role spec against the theme, producing
|
|
58
|
+
* the `{ paragraph, run }` shape the docx library expects.
|
|
59
|
+
*/
|
|
60
|
+
function resolveRoleSpec(spec, theme) {
|
|
61
|
+
const run = {}
|
|
62
|
+
if (spec.bold) run.bold = true
|
|
63
|
+
if (spec.italics) run.italics = true
|
|
64
|
+
if (spec.smallCaps) run.smallCaps = true
|
|
65
|
+
if (spec.allCaps) run.allCaps = true
|
|
66
|
+
if (spec.strike) run.strike = true
|
|
67
|
+
if (spec.size != null) run.size = spec.size
|
|
68
|
+
const color = resolveThemeColor(spec.color, theme)
|
|
69
|
+
if (color) run.color = color
|
|
70
|
+
const font = resolveThemeFont(spec.font, theme)
|
|
71
|
+
if (font) run.font = font
|
|
72
|
+
if (spec.underline) {
|
|
73
|
+
run.underline =
|
|
74
|
+
typeof spec.underline === 'string'
|
|
75
|
+
? { type: spec.underline }
|
|
76
|
+
: spec.underline
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const out = {}
|
|
80
|
+
if (Object.keys(run).length) out.run = run
|
|
81
|
+
if (spec.paragraph) out.paragraph = spec.paragraph
|
|
82
|
+
return out
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Build the `styles` block for `new Document({ styles: … })` from a
|
|
87
|
+
* theme.typography registry.
|
|
88
|
+
*
|
|
89
|
+
* @param {Object} theme
|
|
90
|
+
* @param {Object} [overrides]
|
|
91
|
+
* @param {Array} [overrides.paragraphStyles] - merged on top, caller wins.
|
|
92
|
+
* @param {Array} [overrides.characterStyles] - merged on top, caller wins.
|
|
93
|
+
* @returns {{ default: Object, paragraphStyles: Array, characterStyles: Array }}
|
|
94
|
+
*/
|
|
95
|
+
export function buildStylePack(theme = DEFAULT_THEME, overrides = {}) {
|
|
96
|
+
const typography = theme?.typography || DEFAULT_THEME.typography
|
|
97
|
+
const kinds = theme?.typographyKinds || DEFAULT_THEME.typographyKinds
|
|
98
|
+
|
|
99
|
+
const defaults = {}
|
|
100
|
+
const paragraphStyles = []
|
|
101
|
+
const characterStyles = []
|
|
102
|
+
|
|
103
|
+
for (const [roleName, spec] of Object.entries(typography)) {
|
|
104
|
+
const resolved = resolveRoleSpec(spec, theme)
|
|
105
|
+
const slot = BUILT_IN_DEFAULT_SLOTS[roleName]
|
|
106
|
+
|
|
107
|
+
if (slot === 'document') {
|
|
108
|
+
// Doc-wide defaults — applies to every paragraph and run.
|
|
109
|
+
// The OOXML <w:rPrDefault>/<w:pPrDefault>. This is what
|
|
110
|
+
// pins the body font (no more Times New Roman fallback).
|
|
111
|
+
defaults.document = {
|
|
112
|
+
...(resolved.run ? { run: resolved.run } : {}),
|
|
113
|
+
...(resolved.paragraph ? { paragraph: resolved.paragraph } : {}),
|
|
114
|
+
}
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (slot) {
|
|
119
|
+
// Override a built-in named style (Title, Heading1-6, etc.).
|
|
120
|
+
defaults[slot] = resolved
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Custom role — falls into paragraphStyles or characterStyles.
|
|
125
|
+
const kind = kinds[roleName] || 'character'
|
|
126
|
+
if (kind === 'paragraph') {
|
|
127
|
+
paragraphStyles.push({
|
|
128
|
+
id: roleName,
|
|
129
|
+
name: roleName,
|
|
130
|
+
basedOn: 'Normal',
|
|
131
|
+
next: 'Normal',
|
|
132
|
+
quickFormat: true,
|
|
133
|
+
...resolved,
|
|
134
|
+
})
|
|
135
|
+
} else {
|
|
136
|
+
characterStyles.push({
|
|
137
|
+
id: roleName,
|
|
138
|
+
name: roleName,
|
|
139
|
+
basedOn: 'Normal',
|
|
140
|
+
quickFormat: true,
|
|
141
|
+
...(resolved.run ? { run: resolved.run } : {}),
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Caller-supplied paragraph/character styles take effect in two
|
|
147
|
+
// ways. Entries with built-in style IDs (Title, Heading1-6,
|
|
148
|
+
// Hyperlink, Strong) route to `default.{slot}` since docx ignores
|
|
149
|
+
// those IDs in the custom `paragraphStyles` / `characterStyles`
|
|
150
|
+
// arrays. Everything else appends/overrides in the array.
|
|
151
|
+
const callerParagraphs = overrides.paragraphStyles || []
|
|
152
|
+
const callerCharacters = overrides.characterStyles || []
|
|
153
|
+
|
|
154
|
+
for (const style of [...callerParagraphs, ...callerCharacters]) {
|
|
155
|
+
const slot = BUILT_IN_DEFAULT_SLOTS[style.id]
|
|
156
|
+
if (!slot) continue
|
|
157
|
+
const { id: _id, name: _name, basedOn: _b, next: _n, quickFormat: _q, run, paragraph } =
|
|
158
|
+
style
|
|
159
|
+
defaults[slot] = {
|
|
160
|
+
...(run ? { run } : {}),
|
|
161
|
+
...(paragraph ? { paragraph } : {}),
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const filteredCallerParagraphs = callerParagraphs.filter(
|
|
166
|
+
(s) => !BUILT_IN_DEFAULT_SLOTS[s.id],
|
|
167
|
+
)
|
|
168
|
+
const filteredCallerCharacters = callerCharacters.filter(
|
|
169
|
+
(s) => !BUILT_IN_DEFAULT_SLOTS[s.id],
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
default: defaults,
|
|
174
|
+
paragraphStyles: mergeById(paragraphStyles, filteredCallerParagraphs),
|
|
175
|
+
characterStyles: mergeById(characterStyles, filteredCallerCharacters),
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function mergeById(synthesized, caller) {
|
|
180
|
+
if (!Array.isArray(caller) || caller.length === 0) return synthesized
|
|
181
|
+
const callerById = new Map(caller.map((s) => [s.id, s]))
|
|
182
|
+
const out = synthesized.map((s) =>
|
|
183
|
+
callerById.has(s.id) ? callerById.get(s.id) : s,
|
|
184
|
+
)
|
|
185
|
+
const synthIds = new Set(synthesized.map((s) => s.id))
|
|
186
|
+
for (const s of caller) {
|
|
187
|
+
if (!synthIds.has(s.id)) out.push(s)
|
|
188
|
+
}
|
|
189
|
+
return out
|
|
190
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Math equation — non-text inline atom. Emits a marker span the IR
|
|
3
|
+
* walker turns into a `math` IR node. The typst adapter dispatches on
|
|
4
|
+
* the node and emits `#mitex(`<latex>`)` (inline) or `$ #mitex(`<latex>`) $`
|
|
5
|
+
* (display); foundations consuming `outputs.typst` import mitex in
|
|
6
|
+
* their preamble.
|
|
7
|
+
*
|
|
8
|
+
* Inline and block math share the same builder, distinguished by the
|
|
9
|
+
* `display` flag. The optional `id` flows through as data-id so the
|
|
10
|
+
* typst adapter can emit a typst label (`<id>`) for native
|
|
11
|
+
* cross-references.
|
|
12
|
+
*
|
|
13
|
+
* Mirrors `Image` — same role (non-text inline atom), same attribute
|
|
14
|
+
* convention, same dispatch shape on the adapter side.
|
|
15
|
+
*/
|
|
16
|
+
export default function Math({ latex, display = false, id, ...props }) {
|
|
17
|
+
const attrs = { 'data-type': 'math' }
|
|
18
|
+
attrs['data-latex'] = latex || ''
|
|
19
|
+
attrs['data-display'] = display ? 'true' : 'false'
|
|
20
|
+
if (id) attrs['data-id'] = id
|
|
21
|
+
|
|
22
|
+
return <span {...attrs} {...props} />
|
|
23
|
+
}
|
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 Math from './Math.jsx'
|
|
16
17
|
|
|
17
18
|
export default function Paragraph({ as: Tag = 'p', data, children, ...props }) {
|
|
18
19
|
if (data) {
|
|
@@ -20,17 +21,30 @@ export default function Paragraph({ as: Tag = 'p', data, children, ...props }) {
|
|
|
20
21
|
|
|
21
22
|
return (
|
|
22
23
|
<Tag data-type="paragraph" {...props}>
|
|
23
|
-
{parts.map((part, i) =>
|
|
24
|
-
part.type === 'link'
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
{parts.map((part, i) => {
|
|
25
|
+
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
|
+
)
|
|
36
|
+
}
|
|
37
|
+
if (part.type === 'math') {
|
|
38
|
+
return (
|
|
39
|
+
<Math
|
|
40
|
+
key={i}
|
|
41
|
+
latex={part.latex}
|
|
42
|
+
display={part.display}
|
|
43
|
+
id={part.id}
|
|
44
|
+
/>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
return (
|
|
34
48
|
<TextRun
|
|
35
49
|
key={i}
|
|
36
50
|
bold={part.bold}
|
|
@@ -41,7 +55,7 @@ export default function Paragraph({ as: Tag = 'p', data, children, ...props }) {
|
|
|
41
55
|
{part.content}
|
|
42
56
|
</TextRun>
|
|
43
57
|
)
|
|
44
|
-
)}
|
|
58
|
+
})}
|
|
45
59
|
</Tag>
|
|
46
60
|
)
|
|
47
61
|
}
|
package/src/typst/Sequence.jsx
CHANGED
|
@@ -33,6 +33,7 @@ import CodeBlock from './CodeBlock.jsx'
|
|
|
33
33
|
import List from './List.jsx'
|
|
34
34
|
import BlockQuote from './BlockQuote.jsx'
|
|
35
35
|
import Image from './Image.jsx'
|
|
36
|
+
import Math from './Math.jsx'
|
|
36
37
|
|
|
37
38
|
function renderElement(element, key) {
|
|
38
39
|
if (!element || typeof element !== 'object') return null
|
|
@@ -92,6 +93,20 @@ function renderElement(element, key) {
|
|
|
92
93
|
/>
|
|
93
94
|
)
|
|
94
95
|
|
|
96
|
+
case 'math':
|
|
97
|
+
// Block (display:true) and inline (display:false) share the
|
|
98
|
+
// builder. Inline math doesn't reach the sequence walker —
|
|
99
|
+
// it's wrapped inside paragraph HTML — but render the same
|
|
100
|
+
// way for safety in case a future caller surfaces it here.
|
|
101
|
+
return (
|
|
102
|
+
<Math
|
|
103
|
+
key={key}
|
|
104
|
+
latex={element.latex || ''}
|
|
105
|
+
display={element.display !== false}
|
|
106
|
+
id={element.id || undefined}
|
|
107
|
+
/>
|
|
108
|
+
)
|
|
109
|
+
|
|
95
110
|
// Skipped: dataBlock (not prose), video (not in print), inset (Phase 3).
|
|
96
111
|
default:
|
|
97
112
|
return null
|
package/src/typst/index.js
CHANGED
|
@@ -20,6 +20,7 @@ export { default as CodeBlock } from './CodeBlock.jsx'
|
|
|
20
20
|
export { default as List, BulletList, NumberedList } from './List.jsx'
|
|
21
21
|
export { default as BlockQuote } from './BlockQuote.jsx'
|
|
22
22
|
export { default as Image } from './Image.jsx'
|
|
23
|
+
export { default as Math } from './Math.jsx'
|
|
23
24
|
export { Table, Tr, Td } from './Table.jsx'
|
|
24
25
|
export { default as Asterism } from './Asterism.jsx'
|
|
25
26
|
export { default as Raw } from './Raw.jsx'
|