@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
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) {
|
|
@@ -374,8 +444,11 @@ function irToParagraph(node) {
|
|
|
374
444
|
options.spacing = {}
|
|
375
445
|
const before = toInt(node.spacing.before)
|
|
376
446
|
const after = toInt(node.spacing.after)
|
|
447
|
+
const line = toInt(node.spacing.line)
|
|
377
448
|
if (before != null) options.spacing.before = before
|
|
378
449
|
if (after != null) options.spacing.after = after
|
|
450
|
+
if (line != null) options.spacing.line = line
|
|
451
|
+
if (node.spacing.lineRule) options.spacing.lineRule = node.spacing.lineRule
|
|
379
452
|
}
|
|
380
453
|
if (node.bullet) {
|
|
381
454
|
options.bullet = { level: toInt(node.bullet.level) ?? 0 }
|
|
@@ -388,6 +461,21 @@ function irToParagraph(node) {
|
|
|
388
461
|
const instance = toInt(node.numbering.instance)
|
|
389
462
|
if (instance != null) options.numbering.instance = instance
|
|
390
463
|
}
|
|
464
|
+
if (node.indent) {
|
|
465
|
+
const indent = {}
|
|
466
|
+
for (const key of ['left', 'right', 'start', 'end', 'firstLine', 'hanging']) {
|
|
467
|
+
const v = node.indent[key]
|
|
468
|
+
if (v == null) continue
|
|
469
|
+
const n = typeof v === 'string' ? parseInt(v, 10) : v
|
|
470
|
+
if (Number.isFinite(n)) indent[key] = n
|
|
471
|
+
}
|
|
472
|
+
if (Object.keys(indent).length) options.indent = indent
|
|
473
|
+
}
|
|
474
|
+
if (Array.isArray(node.tabStops) && node.tabStops.length) {
|
|
475
|
+
options.tabStops = node.tabStops
|
|
476
|
+
.map(toTabStopDefinition)
|
|
477
|
+
.filter(Boolean)
|
|
478
|
+
}
|
|
391
479
|
|
|
392
480
|
const children = (node.children || []).flatMap(irToInlineChildren)
|
|
393
481
|
|
|
@@ -416,6 +504,11 @@ function irToInlineChildren(node) {
|
|
|
416
504
|
switch (node.type) {
|
|
417
505
|
case 'text':
|
|
418
506
|
return irToTextRunPair(node)
|
|
507
|
+
case 'tab':
|
|
508
|
+
// Stage 3: <Tab/> inline builder. Emits a docx Tab element
|
|
509
|
+
// that interacts with the paragraph's tabStops. Wrapped in
|
|
510
|
+
// a TextRun so the OOXML parent is <w:r>.
|
|
511
|
+
return [new TextRun({ children: [new DocxTab()] })]
|
|
419
512
|
case 'externalHyperlink':
|
|
420
513
|
return [irToExternalHyperlink(node)]
|
|
421
514
|
case 'internalHyperlink':
|
|
@@ -437,6 +530,14 @@ function irToInlineChildren(node) {
|
|
|
437
530
|
return node.footnoteId
|
|
438
531
|
? [new FootnoteReferenceRun(node.footnoteId)]
|
|
439
532
|
: []
|
|
533
|
+
case 'math': {
|
|
534
|
+
// v1 fallback: emit the LaTeX source as plain text. Better
|
|
535
|
+
// than the MathML soup that was leaking before this case
|
|
536
|
+
// existed (`<mo>…</mo>` operator names appearing as letters
|
|
537
|
+
// in the document). A faithful OMML emitter is tracked in
|
|
538
|
+
// kb/framework/plans/math-print-adapters.md §"Future work".
|
|
539
|
+
return [new TextRun({ text: node.latex || '' })]
|
|
540
|
+
}
|
|
440
541
|
default:
|
|
441
542
|
// Unknown inline: try text extraction, then recurse children.
|
|
442
543
|
if (node.content) {
|
|
@@ -490,6 +591,21 @@ function irToTextRunPair(node) {
|
|
|
490
591
|
if (node.italics === 'true' || node.italics === true) options.italics = true
|
|
491
592
|
if (node.underline) options.underline = node.underline
|
|
492
593
|
if (node.style) options.style = node.style
|
|
594
|
+
// Stage 1 of press-professional-docx: branded text needs color/size/font.
|
|
595
|
+
// The IR's color/size/font come from the default fallthrough rule on
|
|
596
|
+
// unknown data-* attributes (data-color, data-size, data-font set by
|
|
597
|
+
// the TextRun builder). docx@9.x expects: color = hex without '#',
|
|
598
|
+
// size = half-points (28pt = 56), font = family name string.
|
|
599
|
+
if (node.color) options.color = node.color
|
|
600
|
+
if (node.size != null) {
|
|
601
|
+
const sz = typeof node.size === 'string' ? parseInt(node.size, 10) : node.size
|
|
602
|
+
if (Number.isFinite(sz)) options.size = sz
|
|
603
|
+
}
|
|
604
|
+
if (node.font) options.font = node.font
|
|
605
|
+
// Stage 3: smallCaps / allCaps / strike — boolean run formatting.
|
|
606
|
+
if (node.smallCaps === 'true' || node.smallCaps === true) options.smallCaps = true
|
|
607
|
+
if (node.allCaps === 'true' || node.allCaps === true) options.allCaps = true
|
|
608
|
+
if (node.strike === 'true' || node.strike === true) options.strike = true
|
|
493
609
|
|
|
494
610
|
result.push(new TextRun(options))
|
|
495
611
|
return result
|
|
@@ -519,14 +635,53 @@ function irToTable(node) {
|
|
|
519
635
|
const rows = (node.children || [])
|
|
520
636
|
.filter((child) => child.type === 'tableRow')
|
|
521
637
|
.map(irToTableRow)
|
|
522
|
-
|
|
638
|
+
|
|
639
|
+
const options = { rows }
|
|
640
|
+
|
|
641
|
+
// Column widths (twips array). Parsed from `data-table-column-widths`.
|
|
642
|
+
if (Array.isArray(node.tableColumnWidths) && node.tableColumnWidths.length) {
|
|
643
|
+
options.columnWidths = node.tableColumnWidths
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Layout. Default to FIXED whenever explicit column widths are
|
|
647
|
+
// present — Word's autofit otherwise redistributes the columns to
|
|
648
|
+
// fit content, undoing the foundation's layout intent. Foundations
|
|
649
|
+
// that want autofit can opt out with `layout="autofit"`.
|
|
650
|
+
if (node.tableLayout) {
|
|
651
|
+
options.layout =
|
|
652
|
+
node.tableLayout === 'autofit'
|
|
653
|
+
? TableLayoutType.AUTOFIT
|
|
654
|
+
: TableLayoutType.FIXED
|
|
655
|
+
} else if (options.columnWidths) {
|
|
656
|
+
options.layout = TableLayoutType.FIXED
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Whole-table width (distinct from per-cell width).
|
|
660
|
+
if (node.tableWidth) {
|
|
661
|
+
options.width = toTableCellWidth(node.tableWidth)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Table-default borders. Per-cell borders still win.
|
|
665
|
+
if (node.tableBorders) {
|
|
666
|
+
options.borders = toBorders(node.tableBorders)
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return new Table(options)
|
|
523
670
|
}
|
|
524
671
|
|
|
525
672
|
function irToTableRow(node) {
|
|
526
673
|
const children = (node.children || [])
|
|
527
674
|
.filter((child) => child.type === 'tableCell')
|
|
528
675
|
.map(irToTableCell)
|
|
529
|
-
|
|
676
|
+
|
|
677
|
+
const options = { children }
|
|
678
|
+
|
|
679
|
+
// Header rows repeat at the top of each new page when the table breaks.
|
|
680
|
+
if (node.tableHeader) {
|
|
681
|
+
options.tableHeader = true
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
return new TableRow(options)
|
|
530
685
|
}
|
|
531
686
|
|
|
532
687
|
function irToTableCell(node) {
|
|
@@ -541,6 +696,24 @@ function irToTableCell(node) {
|
|
|
541
696
|
if (node.borders) {
|
|
542
697
|
options.borders = toBorders(node.borders)
|
|
543
698
|
}
|
|
699
|
+
if (node.shading) {
|
|
700
|
+
options.shading = toShading(node.shading)
|
|
701
|
+
}
|
|
702
|
+
if (node.verticalAlign) {
|
|
703
|
+
options.verticalAlign = toVerticalAlign(node.verticalAlign)
|
|
704
|
+
}
|
|
705
|
+
// Merge spans. docx's `rowSpan` is the starting-cell shorthand —
|
|
706
|
+
// the library handles the continue cells internally, so we don't
|
|
707
|
+
// need to emit explicit vMerge=continue on subsequent rows from
|
|
708
|
+
// the foundation side.
|
|
709
|
+
if (node.columnSpan) {
|
|
710
|
+
const n = typeof node.columnSpan === 'string' ? parseInt(node.columnSpan, 10) : node.columnSpan
|
|
711
|
+
if (Number.isFinite(n) && n > 1) options.columnSpan = n
|
|
712
|
+
}
|
|
713
|
+
if (node.rowSpan) {
|
|
714
|
+
const n = typeof node.rowSpan === 'string' ? parseInt(node.rowSpan, 10) : node.rowSpan
|
|
715
|
+
if (Number.isFinite(n) && n > 1) options.rowSpan = n
|
|
716
|
+
}
|
|
544
717
|
|
|
545
718
|
// Table cell children must be Paragraph or Table instances.
|
|
546
719
|
const children = (node.children || []).flatMap((child) => {
|
|
@@ -669,6 +842,93 @@ function toBorders(borders) {
|
|
|
669
842
|
return result
|
|
670
843
|
}
|
|
671
844
|
|
|
845
|
+
// --- Cell shading ---
|
|
846
|
+
|
|
847
|
+
const SHADING_TYPES = {
|
|
848
|
+
clear: ShadingType.CLEAR,
|
|
849
|
+
nil: ShadingType.NIL,
|
|
850
|
+
solid: ShadingType.CLEAR, // alias — `solid` is the natural prop name
|
|
851
|
+
diagonalCross: ShadingType.DIAGONAL_CROSS,
|
|
852
|
+
diagonalStripe: ShadingType.DIAGONAL_STRIPE,
|
|
853
|
+
horizontalStripe: ShadingType.HORIZONTAL_STRIPE,
|
|
854
|
+
verticalStripe: ShadingType.VERTICAL_STRIPE,
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Build a TableCell `shading` option from an IR shading object.
|
|
859
|
+
*
|
|
860
|
+
* Foundations almost always want a solid background: `<Td shading="4775b2">`.
|
|
861
|
+
* That JSX shorthand resolves to `{ fill: '4775b2' }` here, and we default
|
|
862
|
+
* `type: CLEAR` and `color: 'auto'` — the standard OOXML idiom for "plain
|
|
863
|
+
* fill, no overlay pattern". Without `type`, Word may render unpredictably
|
|
864
|
+
* across versions; without `color: 'auto'`, the library may emit
|
|
865
|
+
* `w:color=""` which Word repairs on open.
|
|
866
|
+
*/
|
|
867
|
+
function toShading(shading) {
|
|
868
|
+
const fill = shading.fill || '000000'
|
|
869
|
+
const type = SHADING_TYPES[shading.type] ?? ShadingType.CLEAR
|
|
870
|
+
const color = shading.color || 'auto'
|
|
871
|
+
return { type, fill, color }
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// --- Cell vertical alignment ---
|
|
875
|
+
|
|
876
|
+
const VERTICAL_ALIGNS = {
|
|
877
|
+
top: VerticalAlign.TOP,
|
|
878
|
+
center: VerticalAlign.CENTER,
|
|
879
|
+
middle: VerticalAlign.CENTER, // alias — natural-language CSS-ish
|
|
880
|
+
bottom: VerticalAlign.BOTTOM,
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function toVerticalAlign(v) {
|
|
884
|
+
return VERTICAL_ALIGNS[v] ?? VerticalAlign.TOP
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// --- Tab stops ---
|
|
888
|
+
|
|
889
|
+
const TAB_STOP_TYPES = {
|
|
890
|
+
left: TabStopType.LEFT,
|
|
891
|
+
right: TabStopType.RIGHT,
|
|
892
|
+
center: TabStopType.CENTER,
|
|
893
|
+
decimal: TabStopType.DECIMAL,
|
|
894
|
+
bar: TabStopType.BAR,
|
|
895
|
+
clear: TabStopType.CLEAR,
|
|
896
|
+
end: TabStopType.END,
|
|
897
|
+
num: TabStopType.NUM,
|
|
898
|
+
start: TabStopType.START,
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const TAB_STOP_LEADERS = {
|
|
902
|
+
none: LeaderType.NONE,
|
|
903
|
+
dot: LeaderType.DOT,
|
|
904
|
+
hyphen: LeaderType.HYPHEN,
|
|
905
|
+
underscore: LeaderType.UNDERSCORE,
|
|
906
|
+
middleDot: LeaderType.MIDDLE_DOT,
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Build a docx TabStopDefinition from an IR tab-stop entry.
|
|
911
|
+
*
|
|
912
|
+
* Foundations write `tabStops={[{position: cm(12), type: 'right', leader: 'dot'}]}`
|
|
913
|
+
* on a Paragraph. The builder JSON-stringifies the array; the IR
|
|
914
|
+
* transform parses it back here we coerce strings to docx enums.
|
|
915
|
+
*
|
|
916
|
+
* @returns {{type: string, position: number, leader?: string} | null}
|
|
917
|
+
*/
|
|
918
|
+
function toTabStopDefinition(stop) {
|
|
919
|
+
if (!stop || typeof stop !== 'object') return null
|
|
920
|
+
const type = TAB_STOP_TYPES[stop.type] ?? TabStopType.LEFT
|
|
921
|
+
const positionRaw =
|
|
922
|
+
typeof stop.position === 'string' ? parseInt(stop.position, 10) : stop.position
|
|
923
|
+
if (!Number.isFinite(positionRaw)) return null
|
|
924
|
+
const def = { type, position: positionRaw }
|
|
925
|
+
if (stop.leader) {
|
|
926
|
+
const leader = TAB_STOP_LEADERS[stop.leader]
|
|
927
|
+
if (leader) def.leader = leader
|
|
928
|
+
}
|
|
929
|
+
return def
|
|
930
|
+
}
|
|
931
|
+
|
|
672
932
|
// --- Positional tab ---
|
|
673
933
|
|
|
674
934
|
const TAB_ALIGNMENTS = {
|
|
@@ -838,9 +1098,14 @@ async function irToImageParagraph(node, loadAsset) {
|
|
|
838
1098
|
imageOptions.floating = node.floating
|
|
839
1099
|
}
|
|
840
1100
|
|
|
841
|
-
|
|
1101
|
+
const paragraphOptions = {
|
|
842
1102
|
children: [new ImageRun(imageOptions)],
|
|
843
|
-
}
|
|
1103
|
+
}
|
|
1104
|
+
if (node.alignment) {
|
|
1105
|
+
paragraphOptions.alignment = toAlignment(node.alignment)
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
return new DocxParagraph(paragraphOptions)
|
|
844
1109
|
} catch (err) {
|
|
845
1110
|
console.error(`Error creating image element:`, err)
|
|
846
1111
|
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
|
+
}
|