@uniweb/press 0.4.4 → 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/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
package/src/adapters/docx.js
CHANGED
|
@@ -23,6 +23,12 @@ import {
|
|
|
23
23
|
Table,
|
|
24
24
|
TableRow,
|
|
25
25
|
TableCell,
|
|
26
|
+
TableLayoutType,
|
|
27
|
+
VerticalAlign,
|
|
28
|
+
ShadingType,
|
|
29
|
+
Tab as DocxTab,
|
|
30
|
+
TabStopType,
|
|
31
|
+
LeaderType,
|
|
26
32
|
ExternalHyperlink,
|
|
27
33
|
InternalHyperlink,
|
|
28
34
|
ImageRun,
|
|
@@ -45,6 +51,7 @@ import {
|
|
|
45
51
|
FootnoteReferenceRun,
|
|
46
52
|
} from 'docx'
|
|
47
53
|
import { fetchAsset } from '../assets/fetch.js'
|
|
54
|
+
import { buildStylePack } from '../styles/buildStylePack.js'
|
|
48
55
|
|
|
49
56
|
// ============================================================================
|
|
50
57
|
// Public API
|
|
@@ -112,8 +119,13 @@ export async function buildDocument(input, options = {}) {
|
|
|
112
119
|
|
|
113
120
|
const {
|
|
114
121
|
paragraphStyles,
|
|
122
|
+
characterStyles,
|
|
123
|
+
theme,
|
|
124
|
+
typography,
|
|
115
125
|
numbering,
|
|
116
126
|
pageMargin,
|
|
127
|
+
pageSize,
|
|
128
|
+
pageOrientation,
|
|
117
129
|
...documentMetadata
|
|
118
130
|
} = options
|
|
119
131
|
|
|
@@ -137,13 +149,27 @@ export async function buildDocument(input, options = {}) {
|
|
|
137
149
|
const { loadAsset } = options
|
|
138
150
|
const children = await convertChildren(sections.flat(), loadAsset)
|
|
139
151
|
|
|
152
|
+
const pageOptions = {
|
|
153
|
+
pageNumbers: { start: 1, formatType: NumberFormat.DECIMAL },
|
|
154
|
+
...(pageMargin ? { margin: pageMargin } : {}),
|
|
155
|
+
}
|
|
156
|
+
// Stage 4: page size + orientation. docx's IPageSizeAttributes
|
|
157
|
+
// requires width and height, so the orientation flag only takes
|
|
158
|
+
// effect when an explicit pageSize is set. Foundations that want
|
|
159
|
+
// orientation should pass pageSize too — pageSizes.A4 / .LETTER
|
|
160
|
+
// are exported presets.
|
|
161
|
+
if (pageSize) {
|
|
162
|
+
pageOptions.size = {
|
|
163
|
+
width: pageSize.width,
|
|
164
|
+
height: pageSize.height,
|
|
165
|
+
...(pageOrientation ? { orientation: pageOrientation } : {}),
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
140
169
|
const sectionOptions = {
|
|
141
170
|
properties: {
|
|
142
171
|
type: SectionType.CONTINUOUS,
|
|
143
|
-
page:
|
|
144
|
-
pageNumbers: { start: 1, formatType: NumberFormat.DECIMAL },
|
|
145
|
-
...(pageMargin ? { margin: pageMargin } : {}),
|
|
146
|
-
},
|
|
172
|
+
page: pageOptions,
|
|
147
173
|
},
|
|
148
174
|
children,
|
|
149
175
|
}
|
|
@@ -199,8 +225,47 @@ export async function buildDocument(input, options = {}) {
|
|
|
199
225
|
sections: [sectionOptions],
|
|
200
226
|
}
|
|
201
227
|
|
|
202
|
-
|
|
203
|
-
|
|
228
|
+
// Stage 6.0: synthesise OOXML styles from theme.typography (or an
|
|
229
|
+
// explicit typography registry) and merge with caller-supplied
|
|
230
|
+
// paragraphStyles / characterStyles. The synthesised pack provides
|
|
231
|
+
// document defaults (so every run inherits theme.fonts.body without
|
|
232
|
+
// every TextRun emitting `data-font`), built-in style overrides
|
|
233
|
+
// (Word's Title/Heading1-6 routed through `default.*`), and named
|
|
234
|
+
// styles that builders reference via `<Paragraph role="…">` /
|
|
235
|
+
// `<TextRun role="…">`. Caller styles override on id conflict.
|
|
236
|
+
//
|
|
237
|
+
// We synthesise whenever a theme is supplied, even if its
|
|
238
|
+
// typography is partial — buildStylePack falls back to
|
|
239
|
+
// DEFAULT_THEME entries for missing roles so the docx still gets
|
|
240
|
+
// a useful style pack.
|
|
241
|
+
const stylesTheme = typography
|
|
242
|
+
? { ...(theme || {}), typography }
|
|
243
|
+
: theme
|
|
244
|
+
if (stylesTheme) {
|
|
245
|
+
const pack = buildStylePack(stylesTheme, {
|
|
246
|
+
paragraphStyles,
|
|
247
|
+
characterStyles,
|
|
248
|
+
})
|
|
249
|
+
const stylesBlock = {}
|
|
250
|
+
if (pack.default && Object.keys(pack.default).length) {
|
|
251
|
+
stylesBlock.default = pack.default
|
|
252
|
+
}
|
|
253
|
+
if (pack.paragraphStyles.length) {
|
|
254
|
+
stylesBlock.paragraphStyles = pack.paragraphStyles
|
|
255
|
+
}
|
|
256
|
+
if (pack.characterStyles.length) {
|
|
257
|
+
stylesBlock.characterStyles = pack.characterStyles
|
|
258
|
+
}
|
|
259
|
+
if (Object.keys(stylesBlock).length) docOptions.styles = stylesBlock
|
|
260
|
+
} else if (
|
|
261
|
+
(paragraphStyles && paragraphStyles.length) ||
|
|
262
|
+
(characterStyles && characterStyles.length)
|
|
263
|
+
) {
|
|
264
|
+
// Legacy path: caller passed only raw styles, no theme. Pass
|
|
265
|
+
// through verbatim so existing foundations keep working.
|
|
266
|
+
docOptions.styles = {}
|
|
267
|
+
if (paragraphStyles?.length) docOptions.styles.paragraphStyles = paragraphStyles
|
|
268
|
+
if (characterStyles?.length) docOptions.styles.characterStyles = characterStyles
|
|
204
269
|
}
|
|
205
270
|
|
|
206
271
|
if (numbering && numbering.length) {
|
|
@@ -361,7 +426,12 @@ function irToParagraph(node) {
|
|
|
361
426
|
if (node.heading) {
|
|
362
427
|
options.heading = toHeadingLevel(node.heading)
|
|
363
428
|
}
|
|
364
|
-
|
|
429
|
+
// Paragraph-level named style (data-paragraph-style on the element,
|
|
430
|
+
// emitted by `<Paragraph role="…">`). Wins over node.style which is
|
|
431
|
+
// the legacy run-level 'Hyperlink' marker.
|
|
432
|
+
if (node.paragraphStyle) {
|
|
433
|
+
options.style = node.paragraphStyle
|
|
434
|
+
} else if (node.style) {
|
|
365
435
|
options.style = node.style
|
|
366
436
|
}
|
|
367
437
|
if (node.alignment) {
|
|
@@ -388,6 +458,21 @@ function irToParagraph(node) {
|
|
|
388
458
|
const instance = toInt(node.numbering.instance)
|
|
389
459
|
if (instance != null) options.numbering.instance = instance
|
|
390
460
|
}
|
|
461
|
+
if (node.indent) {
|
|
462
|
+
const indent = {}
|
|
463
|
+
for (const key of ['left', 'right', 'start', 'end', 'firstLine', 'hanging']) {
|
|
464
|
+
const v = node.indent[key]
|
|
465
|
+
if (v == null) continue
|
|
466
|
+
const n = typeof v === 'string' ? parseInt(v, 10) : v
|
|
467
|
+
if (Number.isFinite(n)) indent[key] = n
|
|
468
|
+
}
|
|
469
|
+
if (Object.keys(indent).length) options.indent = indent
|
|
470
|
+
}
|
|
471
|
+
if (Array.isArray(node.tabStops) && node.tabStops.length) {
|
|
472
|
+
options.tabStops = node.tabStops
|
|
473
|
+
.map(toTabStopDefinition)
|
|
474
|
+
.filter(Boolean)
|
|
475
|
+
}
|
|
391
476
|
|
|
392
477
|
const children = (node.children || []).flatMap(irToInlineChildren)
|
|
393
478
|
|
|
@@ -416,6 +501,11 @@ function irToInlineChildren(node) {
|
|
|
416
501
|
switch (node.type) {
|
|
417
502
|
case 'text':
|
|
418
503
|
return irToTextRunPair(node)
|
|
504
|
+
case 'tab':
|
|
505
|
+
// Stage 3: <Tab/> inline builder. Emits a docx Tab element
|
|
506
|
+
// that interacts with the paragraph's tabStops. Wrapped in
|
|
507
|
+
// a TextRun so the OOXML parent is <w:r>.
|
|
508
|
+
return [new TextRun({ children: [new DocxTab()] })]
|
|
419
509
|
case 'externalHyperlink':
|
|
420
510
|
return [irToExternalHyperlink(node)]
|
|
421
511
|
case 'internalHyperlink':
|
|
@@ -437,6 +527,14 @@ function irToInlineChildren(node) {
|
|
|
437
527
|
return node.footnoteId
|
|
438
528
|
? [new FootnoteReferenceRun(node.footnoteId)]
|
|
439
529
|
: []
|
|
530
|
+
case 'math': {
|
|
531
|
+
// v1 fallback: emit the LaTeX source as plain text. Better
|
|
532
|
+
// than the MathML soup that was leaking before this case
|
|
533
|
+
// existed (`<mo>…</mo>` operator names appearing as letters
|
|
534
|
+
// in the document). A faithful OMML emitter is tracked in
|
|
535
|
+
// kb/framework/plans/math-print-adapters.md §"Future work".
|
|
536
|
+
return [new TextRun({ text: node.latex || '' })]
|
|
537
|
+
}
|
|
440
538
|
default:
|
|
441
539
|
// Unknown inline: try text extraction, then recurse children.
|
|
442
540
|
if (node.content) {
|
|
@@ -490,6 +588,21 @@ function irToTextRunPair(node) {
|
|
|
490
588
|
if (node.italics === 'true' || node.italics === true) options.italics = true
|
|
491
589
|
if (node.underline) options.underline = node.underline
|
|
492
590
|
if (node.style) options.style = node.style
|
|
591
|
+
// Stage 1 of press-professional-docx: branded text needs color/size/font.
|
|
592
|
+
// The IR's color/size/font come from the default fallthrough rule on
|
|
593
|
+
// unknown data-* attributes (data-color, data-size, data-font set by
|
|
594
|
+
// the TextRun builder). docx@9.x expects: color = hex without '#',
|
|
595
|
+
// size = half-points (28pt = 56), font = family name string.
|
|
596
|
+
if (node.color) options.color = node.color
|
|
597
|
+
if (node.size != null) {
|
|
598
|
+
const sz = typeof node.size === 'string' ? parseInt(node.size, 10) : node.size
|
|
599
|
+
if (Number.isFinite(sz)) options.size = sz
|
|
600
|
+
}
|
|
601
|
+
if (node.font) options.font = node.font
|
|
602
|
+
// Stage 3: smallCaps / allCaps / strike — boolean run formatting.
|
|
603
|
+
if (node.smallCaps === 'true' || node.smallCaps === true) options.smallCaps = true
|
|
604
|
+
if (node.allCaps === 'true' || node.allCaps === true) options.allCaps = true
|
|
605
|
+
if (node.strike === 'true' || node.strike === true) options.strike = true
|
|
493
606
|
|
|
494
607
|
result.push(new TextRun(options))
|
|
495
608
|
return result
|
|
@@ -519,14 +632,53 @@ function irToTable(node) {
|
|
|
519
632
|
const rows = (node.children || [])
|
|
520
633
|
.filter((child) => child.type === 'tableRow')
|
|
521
634
|
.map(irToTableRow)
|
|
522
|
-
|
|
635
|
+
|
|
636
|
+
const options = { rows }
|
|
637
|
+
|
|
638
|
+
// Column widths (twips array). Parsed from `data-table-column-widths`.
|
|
639
|
+
if (Array.isArray(node.tableColumnWidths) && node.tableColumnWidths.length) {
|
|
640
|
+
options.columnWidths = node.tableColumnWidths
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Layout. Default to FIXED whenever explicit column widths are
|
|
644
|
+
// present — Word's autofit otherwise redistributes the columns to
|
|
645
|
+
// fit content, undoing the foundation's layout intent. Foundations
|
|
646
|
+
// that want autofit can opt out with `layout="autofit"`.
|
|
647
|
+
if (node.tableLayout) {
|
|
648
|
+
options.layout =
|
|
649
|
+
node.tableLayout === 'autofit'
|
|
650
|
+
? TableLayoutType.AUTOFIT
|
|
651
|
+
: TableLayoutType.FIXED
|
|
652
|
+
} else if (options.columnWidths) {
|
|
653
|
+
options.layout = TableLayoutType.FIXED
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Whole-table width (distinct from per-cell width).
|
|
657
|
+
if (node.tableWidth) {
|
|
658
|
+
options.width = toTableCellWidth(node.tableWidth)
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Table-default borders. Per-cell borders still win.
|
|
662
|
+
if (node.tableBorders) {
|
|
663
|
+
options.borders = toBorders(node.tableBorders)
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return new Table(options)
|
|
523
667
|
}
|
|
524
668
|
|
|
525
669
|
function irToTableRow(node) {
|
|
526
670
|
const children = (node.children || [])
|
|
527
671
|
.filter((child) => child.type === 'tableCell')
|
|
528
672
|
.map(irToTableCell)
|
|
529
|
-
|
|
673
|
+
|
|
674
|
+
const options = { children }
|
|
675
|
+
|
|
676
|
+
// Header rows repeat at the top of each new page when the table breaks.
|
|
677
|
+
if (node.tableHeader) {
|
|
678
|
+
options.tableHeader = true
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return new TableRow(options)
|
|
530
682
|
}
|
|
531
683
|
|
|
532
684
|
function irToTableCell(node) {
|
|
@@ -541,6 +693,24 @@ function irToTableCell(node) {
|
|
|
541
693
|
if (node.borders) {
|
|
542
694
|
options.borders = toBorders(node.borders)
|
|
543
695
|
}
|
|
696
|
+
if (node.shading) {
|
|
697
|
+
options.shading = toShading(node.shading)
|
|
698
|
+
}
|
|
699
|
+
if (node.verticalAlign) {
|
|
700
|
+
options.verticalAlign = toVerticalAlign(node.verticalAlign)
|
|
701
|
+
}
|
|
702
|
+
// Merge spans. docx's `rowSpan` is the starting-cell shorthand —
|
|
703
|
+
// the library handles the continue cells internally, so we don't
|
|
704
|
+
// need to emit explicit vMerge=continue on subsequent rows from
|
|
705
|
+
// the foundation side.
|
|
706
|
+
if (node.columnSpan) {
|
|
707
|
+
const n = typeof node.columnSpan === 'string' ? parseInt(node.columnSpan, 10) : node.columnSpan
|
|
708
|
+
if (Number.isFinite(n) && n > 1) options.columnSpan = n
|
|
709
|
+
}
|
|
710
|
+
if (node.rowSpan) {
|
|
711
|
+
const n = typeof node.rowSpan === 'string' ? parseInt(node.rowSpan, 10) : node.rowSpan
|
|
712
|
+
if (Number.isFinite(n) && n > 1) options.rowSpan = n
|
|
713
|
+
}
|
|
544
714
|
|
|
545
715
|
// Table cell children must be Paragraph or Table instances.
|
|
546
716
|
const children = (node.children || []).flatMap((child) => {
|
|
@@ -669,6 +839,93 @@ function toBorders(borders) {
|
|
|
669
839
|
return result
|
|
670
840
|
}
|
|
671
841
|
|
|
842
|
+
// --- Cell shading ---
|
|
843
|
+
|
|
844
|
+
const SHADING_TYPES = {
|
|
845
|
+
clear: ShadingType.CLEAR,
|
|
846
|
+
nil: ShadingType.NIL,
|
|
847
|
+
solid: ShadingType.CLEAR, // alias — `solid` is the natural prop name
|
|
848
|
+
diagonalCross: ShadingType.DIAGONAL_CROSS,
|
|
849
|
+
diagonalStripe: ShadingType.DIAGONAL_STRIPE,
|
|
850
|
+
horizontalStripe: ShadingType.HORIZONTAL_STRIPE,
|
|
851
|
+
verticalStripe: ShadingType.VERTICAL_STRIPE,
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Build a TableCell `shading` option from an IR shading object.
|
|
856
|
+
*
|
|
857
|
+
* Foundations almost always want a solid background: `<Td shading="4775b2">`.
|
|
858
|
+
* That JSX shorthand resolves to `{ fill: '4775b2' }` here, and we default
|
|
859
|
+
* `type: CLEAR` and `color: 'auto'` — the standard OOXML idiom for "plain
|
|
860
|
+
* fill, no overlay pattern". Without `type`, Word may render unpredictably
|
|
861
|
+
* across versions; without `color: 'auto'`, the library may emit
|
|
862
|
+
* `w:color=""` which Word repairs on open.
|
|
863
|
+
*/
|
|
864
|
+
function toShading(shading) {
|
|
865
|
+
const fill = shading.fill || '000000'
|
|
866
|
+
const type = SHADING_TYPES[shading.type] ?? ShadingType.CLEAR
|
|
867
|
+
const color = shading.color || 'auto'
|
|
868
|
+
return { type, fill, color }
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// --- Cell vertical alignment ---
|
|
872
|
+
|
|
873
|
+
const VERTICAL_ALIGNS = {
|
|
874
|
+
top: VerticalAlign.TOP,
|
|
875
|
+
center: VerticalAlign.CENTER,
|
|
876
|
+
middle: VerticalAlign.CENTER, // alias — natural-language CSS-ish
|
|
877
|
+
bottom: VerticalAlign.BOTTOM,
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function toVerticalAlign(v) {
|
|
881
|
+
return VERTICAL_ALIGNS[v] ?? VerticalAlign.TOP
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// --- Tab stops ---
|
|
885
|
+
|
|
886
|
+
const TAB_STOP_TYPES = {
|
|
887
|
+
left: TabStopType.LEFT,
|
|
888
|
+
right: TabStopType.RIGHT,
|
|
889
|
+
center: TabStopType.CENTER,
|
|
890
|
+
decimal: TabStopType.DECIMAL,
|
|
891
|
+
bar: TabStopType.BAR,
|
|
892
|
+
clear: TabStopType.CLEAR,
|
|
893
|
+
end: TabStopType.END,
|
|
894
|
+
num: TabStopType.NUM,
|
|
895
|
+
start: TabStopType.START,
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const TAB_STOP_LEADERS = {
|
|
899
|
+
none: LeaderType.NONE,
|
|
900
|
+
dot: LeaderType.DOT,
|
|
901
|
+
hyphen: LeaderType.HYPHEN,
|
|
902
|
+
underscore: LeaderType.UNDERSCORE,
|
|
903
|
+
middleDot: LeaderType.MIDDLE_DOT,
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Build a docx TabStopDefinition from an IR tab-stop entry.
|
|
908
|
+
*
|
|
909
|
+
* Foundations write `tabStops={[{position: cm(12), type: 'right', leader: 'dot'}]}`
|
|
910
|
+
* on a Paragraph. The builder JSON-stringifies the array; the IR
|
|
911
|
+
* transform parses it back here we coerce strings to docx enums.
|
|
912
|
+
*
|
|
913
|
+
* @returns {{type: string, position: number, leader?: string} | null}
|
|
914
|
+
*/
|
|
915
|
+
function toTabStopDefinition(stop) {
|
|
916
|
+
if (!stop || typeof stop !== 'object') return null
|
|
917
|
+
const type = TAB_STOP_TYPES[stop.type] ?? TabStopType.LEFT
|
|
918
|
+
const positionRaw =
|
|
919
|
+
typeof stop.position === 'string' ? parseInt(stop.position, 10) : stop.position
|
|
920
|
+
if (!Number.isFinite(positionRaw)) return null
|
|
921
|
+
const def = { type, position: positionRaw }
|
|
922
|
+
if (stop.leader) {
|
|
923
|
+
const leader = TAB_STOP_LEADERS[stop.leader]
|
|
924
|
+
if (leader) def.leader = leader
|
|
925
|
+
}
|
|
926
|
+
return def
|
|
927
|
+
}
|
|
928
|
+
|
|
672
929
|
// --- Positional tab ---
|
|
673
930
|
|
|
674
931
|
const TAB_ALIGNMENTS = {
|
|
@@ -838,9 +1095,14 @@ async function irToImageParagraph(node, loadAsset) {
|
|
|
838
1095
|
imageOptions.floating = node.floating
|
|
839
1096
|
}
|
|
840
1097
|
|
|
841
|
-
|
|
1098
|
+
const paragraphOptions = {
|
|
842
1099
|
children: [new ImageRun(imageOptions)],
|
|
843
|
-
}
|
|
1100
|
+
}
|
|
1101
|
+
if (node.alignment) {
|
|
1102
|
+
paragraphOptions.alignment = toAlignment(node.alignment)
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
return new DocxParagraph(paragraphOptions)
|
|
844
1106
|
} catch (err) {
|
|
845
1107
|
console.error(`Error creating image element:`, err)
|
|
846
1108
|
return new DocxParagraph({})
|
package/src/adapters/latex.js
CHANGED
|
@@ -379,6 +379,12 @@ function blockNodeToLatex(node) {
|
|
|
379
379
|
case 'link':
|
|
380
380
|
return inlineNodeToLatex(node)
|
|
381
381
|
|
|
382
|
+
case 'math':
|
|
383
|
+
// Bare math at block level (not wrapped in a paragraph). The
|
|
384
|
+
// inline emitter already produces the right `\[…\]` shape
|
|
385
|
+
// for display:true.
|
|
386
|
+
return inlineNodeToLatex(node)
|
|
387
|
+
|
|
382
388
|
case 'paragraph':
|
|
383
389
|
return inlineChildrenToLatex(node.children || []).trim()
|
|
384
390
|
|
|
@@ -596,6 +602,18 @@ function inlineNodeToLatex(node) {
|
|
|
596
602
|
return `\\href{${href}}{${inner}}`
|
|
597
603
|
}
|
|
598
604
|
|
|
605
|
+
if (node.type === 'math') {
|
|
606
|
+
// LaTeX is its own native math source — pass through verbatim.
|
|
607
|
+
// `$…$` for inline; `\[…\]` for display. The optional `\label{}`
|
|
608
|
+
// makes the equation referenceable from elsewhere via `\ref{}`
|
|
609
|
+
// / `\eqref{}`. The `amsmath` package (loaded by the standard
|
|
610
|
+
// preamble) provides `\eqref`; bare `\ref` works without it.
|
|
611
|
+
const display = node.display === 'true' || node.display === true
|
|
612
|
+
const src = node.latex || ''
|
|
613
|
+
const label = node.id ? `\\label{${node.id}}` : ''
|
|
614
|
+
return display ? `\\[${src}${label}\\]` : `$${src}$${label}`
|
|
615
|
+
}
|
|
616
|
+
|
|
599
617
|
return ''
|
|
600
618
|
}
|
|
601
619
|
|
package/src/adapters/typst.js
CHANGED
|
@@ -366,6 +366,12 @@ function blockNodeToTypst(node) {
|
|
|
366
366
|
case 'link':
|
|
367
367
|
return inlineNodeToTypst(node)
|
|
368
368
|
|
|
369
|
+
case 'math':
|
|
370
|
+
// Bare math at block level (when an adapter is fed a single
|
|
371
|
+
// math element rather than a paragraph wrapping one). Reuse
|
|
372
|
+
// the inline emitter so the output stays consistent.
|
|
373
|
+
return inlineNodeToTypst(node)
|
|
374
|
+
|
|
369
375
|
case 'paragraph':
|
|
370
376
|
return inlineChildrenToTypst(node.children || []).trim()
|
|
371
377
|
|
|
@@ -496,8 +502,14 @@ function inlineNodeToTypst(node) {
|
|
|
496
502
|
if (node.type === 'text') {
|
|
497
503
|
let text = escapeTypstInline(node.content || '')
|
|
498
504
|
if (node.code === 'true') text = '`' + (node.content || '') + '`'
|
|
499
|
-
|
|
500
|
-
|
|
505
|
+
// Use the function form `#strong[…]` / `#emph[…]` instead of the
|
|
506
|
+
// markup form `*…*` / `_…_`. Typst's markup-mode marks fail to
|
|
507
|
+
// close when the closing character butts directly against an
|
|
508
|
+
// alphanumeric (e.g. `_journal_4` keeps emphasis open through the
|
|
509
|
+
// rest of the document, producing "unclosed delimiter" errors).
|
|
510
|
+
// The function form has no boundary requirement.
|
|
511
|
+
if (node.bold === 'true') text = `#strong[${text}]`
|
|
512
|
+
if (node.italics === 'true') text = `#emph[${text}]`
|
|
501
513
|
if (node.underline && (node.underline === 'true' || typeof node.underline === 'object')) {
|
|
502
514
|
text = `#underline[${text}]`
|
|
503
515
|
}
|
|
@@ -510,6 +522,26 @@ function inlineNodeToTypst(node) {
|
|
|
510
522
|
return `#link(${quoteTypstString(href)})[${inner}]`
|
|
511
523
|
}
|
|
512
524
|
|
|
525
|
+
if (node.type === 'math') {
|
|
526
|
+
// Math is delegated to the mitex typst package — foundations
|
|
527
|
+
// import mitex in their preamble. mitex emits typst math
|
|
528
|
+
// content (not arbitrary markup), so the call goes inside a
|
|
529
|
+
// typst math context: `$#mitex(\`…\`)$` for inline (no space
|
|
530
|
+
// around the dollars), `$ #mitex(\`…\`) $` for display (the
|
|
531
|
+
// spaces are typst's own inline-vs-display switch).
|
|
532
|
+
//
|
|
533
|
+
// The latex source goes inside backticks (a typst raw string).
|
|
534
|
+
// mitex parses LaTeX from the raw string and re-emits typst
|
|
535
|
+
// math. No JS-side LaTeX → typst translation needed.
|
|
536
|
+
const display = node.display === 'true' || node.display === true
|
|
537
|
+
const src = node.latex || ''
|
|
538
|
+
const labelSuffix = node.id ? ` <${node.id}>` : ''
|
|
539
|
+
if (display) {
|
|
540
|
+
return `\n$ #mitex(\`${src}\`) $${labelSuffix}\n`
|
|
541
|
+
}
|
|
542
|
+
return `$#mitex(\`${src}\`)$${labelSuffix}`
|
|
543
|
+
}
|
|
544
|
+
|
|
513
545
|
return ''
|
|
514
546
|
}
|
|
515
547
|
|
|
@@ -56,13 +56,13 @@ import { runCompile, getAdapterDescriptor } from './adapters/dispatch.js'
|
|
|
56
56
|
* const blob = await compileTypst(compiled, { mode: 'sources', ... })
|
|
57
57
|
*/
|
|
58
58
|
export function compileRegistrations(elements, format, options = {}) {
|
|
59
|
-
const { basePath } = options
|
|
59
|
+
const { basePath, theme } = options
|
|
60
60
|
const store = createStore()
|
|
61
61
|
|
|
62
62
|
renderToStaticMarkup(
|
|
63
63
|
createElement(
|
|
64
64
|
DocumentProvider,
|
|
65
|
-
{ store, basePath },
|
|
65
|
+
{ store, basePath, theme },
|
|
66
66
|
elements,
|
|
67
67
|
),
|
|
68
68
|
)
|
|
@@ -138,8 +138,8 @@ function warnIfEmptyRegistrations(store, format) {
|
|
|
138
138
|
* triggerDownload(blob, 'book.zip')
|
|
139
139
|
*/
|
|
140
140
|
export async function compileSubtree(elements, format, options = {}) {
|
|
141
|
-
const { basePath, adapterOptions = {} } = options
|
|
142
|
-
const compiled = compileRegistrations(elements, format, { basePath })
|
|
141
|
+
const { basePath, theme, adapterOptions = {} } = options
|
|
142
|
+
const compiled = compileRegistrations(elements, format, { basePath, theme })
|
|
143
143
|
return runCompile(format, compiled, adapterOptions)
|
|
144
144
|
}
|
|
145
145
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrandLogo — a brand image suitable for page headers, cover blocks,
|
|
3
|
+
* and watermarks.
|
|
4
|
+
*
|
|
5
|
+
* Stage 6.4 of kb/framework/plans/press-professional-docx.md.
|
|
6
|
+
*
|
|
7
|
+
* Emits a bare `<img data-type="image">` (no Paragraph wrapper) so
|
|
8
|
+
* the IR walker keeps the image at section level — Press's docx
|
|
9
|
+
* adapter creates the carrier paragraph itself, and inline-context
|
|
10
|
+
* images are silently dropped (Image.jsx's documented contract).
|
|
11
|
+
*
|
|
12
|
+
* Alignment flows through the IR's `data-alignment` channel: the
|
|
13
|
+
* adapter applies it to the carrier paragraph before emitting the
|
|
14
|
+
* ImageRun. Foundations get a right-aligned wordmark with one prop:
|
|
15
|
+
*
|
|
16
|
+
* <BrandLogo url={vendor.logo} width={cm(4)} align="right" />
|
|
17
|
+
*
|
|
18
|
+
* The legacy PHP-rendered Proximify invoice used a 5cm logo right-
|
|
19
|
+
* anchored in the page header. The default values match that visual.
|
|
20
|
+
*/
|
|
21
|
+
import Image from './Image.jsx'
|
|
22
|
+
|
|
23
|
+
const DEFAULT_WIDTH = 180 // ~5 cm at the docx default DPI
|
|
24
|
+
const DEFAULT_HEIGHT_RATIO = 0.32 // typical horizontal-wordmark aspect
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {Object} props
|
|
28
|
+
* @param {string} [props.url] - Image source. Resolved against
|
|
29
|
+
* DocumentProvider's basePath if site-absolute. When absent, the
|
|
30
|
+
* builder renders nothing.
|
|
31
|
+
* @param {string} [props.alt] - Accessibility text. Defaults to ''
|
|
32
|
+
* (decorative).
|
|
33
|
+
* @param {number} [props.width=180] - Width in docx transformation
|
|
34
|
+
* units. Use cm()/inch()/pt() for natural values.
|
|
35
|
+
* @param {number} [props.height] - Height. When omitted, defaults to
|
|
36
|
+
* width * 0.32 (typical horizontal-wordmark aspect).
|
|
37
|
+
* @param {'left'|'center'|'right'} [props.align='right'] - Alignment of
|
|
38
|
+
* the carrier paragraph the docx adapter wraps the ImageRun in.
|
|
39
|
+
*/
|
|
40
|
+
export default function BrandLogo({
|
|
41
|
+
url,
|
|
42
|
+
alt = '',
|
|
43
|
+
width = DEFAULT_WIDTH,
|
|
44
|
+
height,
|
|
45
|
+
align = 'right',
|
|
46
|
+
...rest
|
|
47
|
+
}) {
|
|
48
|
+
if (!url) return null
|
|
49
|
+
const h = height ?? Math.round(width * DEFAULT_HEIGHT_RATIO)
|
|
50
|
+
return (
|
|
51
|
+
<Image
|
|
52
|
+
data={{ url, alt }}
|
|
53
|
+
width={width}
|
|
54
|
+
height={h}
|
|
55
|
+
data-alignment={align}
|
|
56
|
+
{...rest}
|
|
57
|
+
/>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Math equation — non-text inline atom. Emits a marker span the IR
|
|
3
|
+
* walker turns into a `math` IR node. The docx adapter's v1 fallback
|
|
4
|
+
* emits the LaTeX source as plain text (better than the mathml soup
|
|
5
|
+
* it was leaking before this builder existed); a faithful OMML emitter
|
|
6
|
+
* is tracked in kb/framework/plans/math-print-adapters.md §"Future work".
|
|
7
|
+
*
|
|
8
|
+
* Inline and block math share the same builder, distinguished by the
|
|
9
|
+
* `display` flag. The optional `id` flows through for parity with the
|
|
10
|
+
* typst/latex builders even though docx v1 doesn't use it.
|
|
11
|
+
*
|
|
12
|
+
* Mirrors `Image` — same role (non-text inline atom), same attribute
|
|
13
|
+
* convention, same dispatch shape on the adapter side.
|
|
14
|
+
*/
|
|
15
|
+
export default function Math({ latex, display = false, id, ...props }) {
|
|
16
|
+
const attrs = { 'data-type': 'math' }
|
|
17
|
+
attrs['data-latex'] = latex || ''
|
|
18
|
+
attrs['data-display'] = display ? 'true' : 'false'
|
|
19
|
+
if (id) attrs['data-id'] = id
|
|
20
|
+
|
|
21
|
+
return <span {...attrs} {...props} />
|
|
22
|
+
}
|