@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.
@@ -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
+ }
@@ -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
- <a
26
- key={i}
27
- data-type="link"
28
- data-href={part.href}
29
- href={part.href}
30
- >
31
- <span data-type="text">{part.content}</span>
32
- </a>
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
  }
@@ -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
@@ -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'