@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
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Parses a styled string with inline HTML marks (<strong>, <em>, <u>, <a
|
|
3
|
-
* into an array of text/link
|
|
2
|
+
* Parses a styled string with inline HTML marks (<strong>, <em>, <u>, <a>,
|
|
3
|
+
* <span data-type="math">) into an array of text / link / math part
|
|
4
|
+
* objects with style flags.
|
|
4
5
|
*
|
|
5
6
|
* Ported from legacy report-sdk/src/utils.js:116-186, extended to support
|
|
6
7
|
* <a href="..."> hyperlinks so paragraphs with auto-linked emails and URLs
|
|
7
|
-
* produce real hyperlinks in the docx output
|
|
8
|
+
* produce real hyperlinks in the docx output, and <span data-type="math">
|
|
9
|
+
* so inline math reaches the print adapters as a structured atom rather
|
|
10
|
+
* than being walked as opaque DOM (which turns MathML into raw operator
|
|
11
|
+
* text).
|
|
8
12
|
*
|
|
9
13
|
* @param {string} inputString - HTML string with inline marks.
|
|
10
|
-
* @returns {Array<{type: string, content
|
|
14
|
+
* @returns {Array<{type: string, content?: string, bold?: boolean, italics?: boolean, underline?: object, href?: string, latex?: string, display?: boolean}>}
|
|
11
15
|
*
|
|
12
16
|
* @example
|
|
13
17
|
* parseStyledString('Hello <strong>World</strong>')
|
|
@@ -22,7 +26,34 @@
|
|
|
22
26
|
* // { type: 'text', content: 'Visit ' },
|
|
23
27
|
* // { type: 'link', content: 'Example', href: 'https://example.com' }
|
|
24
28
|
* // ]
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* parseStyledString('Let $G$ be a graph') // after semantic-parser wrapping
|
|
32
|
+
* // input: 'Let <span data-type="math" data-latex="G" data-display="false">…mathml…</span> be a graph'
|
|
33
|
+
* // => [
|
|
34
|
+
* // { type: 'text', content: 'Let ' },
|
|
35
|
+
* // { type: 'math', latex: 'G', display: false },
|
|
36
|
+
* // { type: 'text', content: ' be a graph' }
|
|
37
|
+
* // ]
|
|
25
38
|
*/
|
|
39
|
+
|
|
40
|
+
// Decode HTML-attribute entities written by sequence.js's escapeAttr().
|
|
41
|
+
// Inverse of the four replacements there. `&` last so a literal
|
|
42
|
+
// `&quot;` round-trips correctly.
|
|
43
|
+
function decodeAttr(s) {
|
|
44
|
+
return String(s)
|
|
45
|
+
.replace(/"/g, '"')
|
|
46
|
+
.replace(/</g, '<')
|
|
47
|
+
.replace(/>/g, '>')
|
|
48
|
+
.replace(/&/g, '&')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readAttr(attrs, name) {
|
|
52
|
+
const re = new RegExp(`${name}="([^"]*)"`)
|
|
53
|
+
const m = attrs.match(re)
|
|
54
|
+
return m ? decodeAttr(m[1]) : null
|
|
55
|
+
}
|
|
56
|
+
|
|
26
57
|
export function parseStyledString(inputString) {
|
|
27
58
|
const createTextPart = (content, styles) => ({
|
|
28
59
|
type: 'text',
|
|
@@ -48,8 +79,7 @@ export function parseStyledString(inputString) {
|
|
|
48
79
|
|
|
49
80
|
// Handle <a> tags as links
|
|
50
81
|
if (tag === 'a' && attrs) {
|
|
51
|
-
const
|
|
52
|
-
const href = hrefMatch?.[1]
|
|
82
|
+
const href = readAttr(attrs, 'href')
|
|
53
83
|
if (href) {
|
|
54
84
|
result.push({
|
|
55
85
|
type: 'link',
|
|
@@ -61,6 +91,29 @@ export function parseStyledString(inputString) {
|
|
|
61
91
|
}
|
|
62
92
|
}
|
|
63
93
|
|
|
94
|
+
// Handle <span data-type="math"> as an inline atom. The inner
|
|
95
|
+
// text is the pre-compiled MathML — discarded here because
|
|
96
|
+
// print adapters consume the LaTeX source instead. Browser-
|
|
97
|
+
// side renderers don't go through parseStyledString; they use
|
|
98
|
+
// dangerouslySetInnerHTML on the original paragraph HTML where
|
|
99
|
+
// the MathML survives intact.
|
|
100
|
+
if (tag === 'span' && attrs) {
|
|
101
|
+
const dataType = readAttr(attrs, 'data-type')
|
|
102
|
+
if (dataType === 'math') {
|
|
103
|
+
const latex = readAttr(attrs, 'data-latex') || ''
|
|
104
|
+
const display = readAttr(attrs, 'data-display') === 'true'
|
|
105
|
+
const id = readAttr(attrs, 'data-id')
|
|
106
|
+
result.push({
|
|
107
|
+
type: 'math',
|
|
108
|
+
latex,
|
|
109
|
+
display,
|
|
110
|
+
...(id ? { id } : {}),
|
|
111
|
+
})
|
|
112
|
+
lastIndex = offset + match.length
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
64
117
|
const newStyles = { ...styles }
|
|
65
118
|
if (tag === 'strong' || tag === 'b') newStyles.bold = true
|
|
66
119
|
if (tag === 'em' || tag === 'i') newStyles.italics = true
|
package/src/docx/units.js
CHANGED
|
@@ -73,3 +73,24 @@ export function convertCentimetersToTwip(centimeters) {
|
|
|
73
73
|
export function convertPointsToHalfPoints(points) {
|
|
74
74
|
return Math.round(points * 2)
|
|
75
75
|
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Short aliases — for terse JSX. The longer names above remain canonical
|
|
79
|
+
// (and match the docx library's own helpers); these are sugar so foundation
|
|
80
|
+
// code can write `columnWidths={[cm(15), cm(4), cm(1.5), cm(4)]}` instead of
|
|
81
|
+
// `[convertCentimetersToTwip(15), convertCentimetersToTwip(4), …]`.
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
/** Centimeters to twips. Alias of `convertCentimetersToTwip`. */
|
|
85
|
+
export const cm = convertCentimetersToTwip
|
|
86
|
+
|
|
87
|
+
/** Millimeters to twips. Alias of `convertMillimetersToTwip`. */
|
|
88
|
+
export const mm = convertMillimetersToTwip
|
|
89
|
+
|
|
90
|
+
/** Inches to twips. Alias of `convertInchesToTwip`. */
|
|
91
|
+
export const inch = convertInchesToTwip
|
|
92
|
+
|
|
93
|
+
/** Points to twips. There are 20 twips in a point. */
|
|
94
|
+
export function pt(points) {
|
|
95
|
+
return Math.round(points * 20)
|
|
96
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @uniweb/press/format — locale-aware text formatters.
|
|
3
|
+
*
|
|
4
|
+
* Stage 6.1 of kb/framework/plans/press-professional-docx.md.
|
|
5
|
+
*
|
|
6
|
+
* The same JSX feeds the React preview and the docx fragment, so
|
|
7
|
+
* these are React components that emit <span data-type="text"> and
|
|
8
|
+
* route through the standard IR walker. Each takes a `value` prop
|
|
9
|
+
* (or `from`/`to` for ranges) plus optional locale + format props,
|
|
10
|
+
* and produces a single text node with the formatted string. They
|
|
11
|
+
* are theme-aware: an active <DocumentProvider> supplies the
|
|
12
|
+
* default locale and (for currency) the default ISO code, so
|
|
13
|
+
* foundations don't have to repeat them at every call site.
|
|
14
|
+
*
|
|
15
|
+
* The motivation: foundations had been writing `String(invoice.due)`
|
|
16
|
+
* which yields `"Sat Feb 28 2026 19:00:00 GMT-0500 …"` whenever
|
|
17
|
+
* YAML eagerly turns an ISO string into a JS Date. The IR layer
|
|
18
|
+
* also has a defensive Date->ISO coercion (src/adapters/docx.js),
|
|
19
|
+
* but the right way to render dates is to choose a format
|
|
20
|
+
* explicitly via these builders.
|
|
21
|
+
*/
|
|
22
|
+
import { useDocumentTheme } from '../ThemeContext.js'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Coerce a value to a JS Date for formatting. Accepts:
|
|
26
|
+
* - Date instance (used as-is)
|
|
27
|
+
* - ISO string ('2026-03-31')
|
|
28
|
+
* - epoch number
|
|
29
|
+
* Returns null on invalid input so the builder renders nothing.
|
|
30
|
+
*/
|
|
31
|
+
function toDate(value) {
|
|
32
|
+
if (value == null || value === '') return null
|
|
33
|
+
if (value instanceof Date) {
|
|
34
|
+
return Number.isNaN(value.getTime()) ? null : value
|
|
35
|
+
}
|
|
36
|
+
const d = new Date(value)
|
|
37
|
+
return Number.isNaN(d.getTime()) ? null : d
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Map our small set of format names onto Intl.DateTimeFormat option
|
|
42
|
+
* bags. 'iso' is special-cased — it returns the YYYY-MM-DD form
|
|
43
|
+
* exactly, using UTC components (not local) so a date like
|
|
44
|
+
* `2026-03-31` doesn't shift across timezones.
|
|
45
|
+
*/
|
|
46
|
+
const DATE_FORMATS = {
|
|
47
|
+
iso: null, // sentinel — handled before Intl
|
|
48
|
+
short: { year: 'numeric', month: 'numeric', day: 'numeric' },
|
|
49
|
+
medium: { year: 'numeric', month: 'short', day: 'numeric' },
|
|
50
|
+
long: { year: 'numeric', month: 'long', day: 'numeric' },
|
|
51
|
+
full: {
|
|
52
|
+
weekday: 'long',
|
|
53
|
+
year: 'numeric',
|
|
54
|
+
month: 'long',
|
|
55
|
+
day: 'numeric',
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function formatIso(d) {
|
|
60
|
+
const y = d.getUTCFullYear()
|
|
61
|
+
const m = String(d.getUTCMonth() + 1).padStart(2, '0')
|
|
62
|
+
const day = String(d.getUTCDate()).padStart(2, '0')
|
|
63
|
+
return `${y}-${m}-${day}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function formatDate(value, { locale = 'en-CA', format = 'medium' } = {}) {
|
|
67
|
+
const d = toDate(value)
|
|
68
|
+
if (!d) return ''
|
|
69
|
+
if (format === 'iso') return formatIso(d)
|
|
70
|
+
const opts = DATE_FORMATS[format] || DATE_FORMATS.medium
|
|
71
|
+
try {
|
|
72
|
+
// Force UTC so date-only ISO inputs ('2026-03-31') don't drift
|
|
73
|
+
// by the local timezone — without this, en-CA in EST renders
|
|
74
|
+
// 'Mar 30, 2026' instead of 'Mar 31, 2026'.
|
|
75
|
+
return new Intl.DateTimeFormat(locale, { ...opts, timeZone: 'UTC' }).format(d)
|
|
76
|
+
} catch {
|
|
77
|
+
// Bad locale — fall back to ISO.
|
|
78
|
+
return formatIso(d)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function formatDateRange(from, to, opts) {
|
|
83
|
+
const f = formatDate(from, opts)
|
|
84
|
+
const t = formatDate(to, opts)
|
|
85
|
+
if (f && t) return `${f} – ${t}`
|
|
86
|
+
return f || t || ''
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function formatCurrency(value, { locale = 'en-CA', code = 'CAD' } = {}) {
|
|
90
|
+
if (value == null || value === '') return ''
|
|
91
|
+
const n = typeof value === 'number' ? value : Number(value)
|
|
92
|
+
if (!Number.isFinite(n)) return ''
|
|
93
|
+
try {
|
|
94
|
+
return new Intl.NumberFormat(locale, {
|
|
95
|
+
style: 'currency',
|
|
96
|
+
currency: code,
|
|
97
|
+
maximumFractionDigits: 2,
|
|
98
|
+
}).format(n)
|
|
99
|
+
} catch {
|
|
100
|
+
// Unknown currency code — fall back to plain number with the code.
|
|
101
|
+
return `${n.toFixed(2)} ${code}`
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Inline date — emits a single text node with a formatted date.
|
|
107
|
+
*
|
|
108
|
+
* @param {Object} props
|
|
109
|
+
* @param {Date|string|number} props.value
|
|
110
|
+
* @param {'iso'|'short'|'medium'|'long'|'full'} [props.format='medium']
|
|
111
|
+
* @param {string} [props.locale] - BCP-47. Defaults to theme.locale.
|
|
112
|
+
*/
|
|
113
|
+
export function DateText({ value, format = 'medium', locale }) {
|
|
114
|
+
const theme = useDocumentTheme()
|
|
115
|
+
const text = formatDate(value, {
|
|
116
|
+
locale: locale ?? theme.locale,
|
|
117
|
+
format,
|
|
118
|
+
})
|
|
119
|
+
return <span data-type="text">{text}</span>
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Inline date range — emits a single text node with two formatted
|
|
124
|
+
* dates joined by an en dash. When one bound is missing, only the
|
|
125
|
+
* other one renders.
|
|
126
|
+
*/
|
|
127
|
+
export function DateRangeText({
|
|
128
|
+
from,
|
|
129
|
+
to,
|
|
130
|
+
period,
|
|
131
|
+
format = 'medium',
|
|
132
|
+
locale,
|
|
133
|
+
}) {
|
|
134
|
+
const theme = useDocumentTheme()
|
|
135
|
+
const f = period?.from ?? from
|
|
136
|
+
const t = period?.to ?? to
|
|
137
|
+
const text = formatDateRange(f, t, {
|
|
138
|
+
locale: locale ?? theme.locale,
|
|
139
|
+
format,
|
|
140
|
+
})
|
|
141
|
+
return <span data-type="text">{text}</span>
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Inline currency — emits a single text node with a locale- and
|
|
146
|
+
* code-aware money amount.
|
|
147
|
+
*
|
|
148
|
+
* @param {Object} props
|
|
149
|
+
* @param {number|string} props.value
|
|
150
|
+
* @param {string} [props.code] - ISO 4217. Defaults to theme.currency.
|
|
151
|
+
* @param {string} [props.locale]
|
|
152
|
+
*/
|
|
153
|
+
export function Currency({ value, code, locale }) {
|
|
154
|
+
const theme = useDocumentTheme()
|
|
155
|
+
const text = formatCurrency(value, {
|
|
156
|
+
locale: locale ?? theme.locale,
|
|
157
|
+
code: code ?? theme.currency,
|
|
158
|
+
})
|
|
159
|
+
return <span data-type="text">{text}</span>
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Imperative versions for callers that need a string outside JSX
|
|
164
|
+
* (cell content building, header data, etc.). These don't touch the
|
|
165
|
+
* theme — pass locale/code explicitly.
|
|
166
|
+
*/
|
|
167
|
+
export const formatters = {
|
|
168
|
+
date: formatDate,
|
|
169
|
+
dateRange: formatDateRange,
|
|
170
|
+
currency: formatCurrency,
|
|
171
|
+
}
|
package/src/index.js
CHANGED
package/src/ir/attributes.js
CHANGED
|
@@ -46,6 +46,23 @@ const toInt = (v) => {
|
|
|
46
46
|
return Number.isFinite(n) ? n : v
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Parse a comma-separated list of integers (whitespace tolerant).
|
|
51
|
+
* Used by `data-table-column-widths`, where the attribute encodes an
|
|
52
|
+
* array of twip values like "8504,2268,851,2268". Empty / non-numeric
|
|
53
|
+
* tokens are dropped — downstream callers treat an empty array the
|
|
54
|
+
* same as "no column widths declared".
|
|
55
|
+
*/
|
|
56
|
+
const parseIntList = (v) => {
|
|
57
|
+
if (typeof v !== 'string') return v
|
|
58
|
+
return v
|
|
59
|
+
.split(',')
|
|
60
|
+
.map((s) => s.trim())
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.map((s) => parseInt(s, 10))
|
|
63
|
+
.filter((n) => Number.isFinite(n))
|
|
64
|
+
}
|
|
65
|
+
|
|
49
66
|
/**
|
|
50
67
|
* Explicit attribute → IR path mapping. Entries are listed in the same
|
|
51
68
|
* order as the legacy switch for ease of cross-reference.
|
|
@@ -94,6 +111,124 @@ export const attributeMap = {
|
|
|
94
111
|
'data-borders-right-size': { path: ['borders', 'right', 'size'] },
|
|
95
112
|
'data-borders-right-color': { path: ['borders', 'right', 'color'] },
|
|
96
113
|
|
|
114
|
+
// ------------------------------------------------------------------
|
|
115
|
+
// Table-cell additions (Stage 1 of the press-professional-docx plan)
|
|
116
|
+
// ------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
// Cell shading. `data-shading-fill` is the load-bearing attribute
|
|
119
|
+
// (the solid background color). `data-shading-type` defaults to
|
|
120
|
+
// 'clear' in the adapter so a plain fill JSX prop produces a solid
|
|
121
|
+
// background without callers having to remember the OOXML idiom.
|
|
122
|
+
'data-shading-fill': { path: ['shading', 'fill'] },
|
|
123
|
+
'data-shading-color': { path: ['shading', 'color'] },
|
|
124
|
+
'data-shading-type': { path: ['shading', 'type'] },
|
|
125
|
+
|
|
126
|
+
// Vertical alignment of the cell's content. `top` | `center` | `bottom`.
|
|
127
|
+
// Maps to docx's VerticalAlignTable enum at the adapter layer.
|
|
128
|
+
'data-valign': { path: ['verticalAlign'] },
|
|
129
|
+
|
|
130
|
+
// Cell column merge: how many adjacent columns this cell spans.
|
|
131
|
+
'data-grid-span': { path: ['columnSpan'], transform: toInt },
|
|
132
|
+
|
|
133
|
+
// Cell row merge: how many rows this cell spans (vertical merge).
|
|
134
|
+
// The adapter uses docx's `rowSpan` shorthand, which expects only
|
|
135
|
+
// the *starting* cell to declare a count; library handles the
|
|
136
|
+
// merge-continue rows internally.
|
|
137
|
+
'data-row-span': { path: ['rowSpan'], transform: toInt },
|
|
138
|
+
|
|
139
|
+
// ------------------------------------------------------------------
|
|
140
|
+
// Stage 3 — paragraph polish
|
|
141
|
+
// ------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
// Paragraph indentation. All values are twips. `firstLine` and
|
|
144
|
+
// `hanging` are positive; `left` and `right` may be negative.
|
|
145
|
+
'data-indent-left': { path: ['indent', 'left'], transform: toInt },
|
|
146
|
+
'data-indent-right': { path: ['indent', 'right'], transform: toInt },
|
|
147
|
+
'data-indent-firstline': { path: ['indent', 'firstLine'], transform: toInt },
|
|
148
|
+
'data-indent-hanging': { path: ['indent', 'hanging'], transform: toInt },
|
|
149
|
+
|
|
150
|
+
// Paragraph tab stops. JSON-encoded array of TabStopDefinition objects:
|
|
151
|
+
// [{ position: 6804, type: 'right', leader: 'dot' }, …]
|
|
152
|
+
// Position is in twips; type is left/right/center/decimal/etc;
|
|
153
|
+
// leader is none/dot/hyphen/underscore/middleDot. Foundations
|
|
154
|
+
// typically construct these via the <Paragraph tabStops=…> prop
|
|
155
|
+
// and the unit helpers (cm, mm, inch, pt).
|
|
156
|
+
// Paragraph-level named style ('Title', 'Heading1', 'Body', …).
|
|
157
|
+
// Maps to `<w:pStyle w:val="…"/>` in the docx adapter. Distinct
|
|
158
|
+
// from the run-level `data-style` (default fallthrough), which
|
|
159
|
+
// emits `<w:rStyle>`.
|
|
160
|
+
'data-paragraph-style': { path: ['paragraphStyle'] },
|
|
161
|
+
|
|
162
|
+
'data-tab-stops': {
|
|
163
|
+
path: ['tabStops'],
|
|
164
|
+
transform: (v) => {
|
|
165
|
+
if (typeof v !== 'string') return v
|
|
166
|
+
try {
|
|
167
|
+
const parsed = JSON.parse(v)
|
|
168
|
+
return Array.isArray(parsed) ? parsed : v
|
|
169
|
+
} catch {
|
|
170
|
+
return v
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
// TextRun toggles — explicit map entries route the lower-case
|
|
176
|
+
// HTML attribute names to camelCase IR fields the adapter reads.
|
|
177
|
+
'data-smallcaps': { path: ['smallCaps'], transform: asTrue },
|
|
178
|
+
'data-allcaps': { path: ['allCaps'], transform: asTrue },
|
|
179
|
+
'data-strike': { path: ['strike'], transform: asTrue },
|
|
180
|
+
|
|
181
|
+
// Row-level: whether the row repeats as a header on each new page
|
|
182
|
+
// when the table breaks. Presence-only — any truthy value counts.
|
|
183
|
+
'data-row-header': { path: ['tableHeader'], transform: asTrue },
|
|
184
|
+
|
|
185
|
+
// ------------------------------------------------------------------
|
|
186
|
+
// Table-level options (Stage 1)
|
|
187
|
+
//
|
|
188
|
+
// Stored under the prefixed `tableXxx` namespace so they don't
|
|
189
|
+
// collide with the cell-level `width` / `borders` properties that
|
|
190
|
+
// existed before. `irToTable` reads these on table-typed nodes.
|
|
191
|
+
// ------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
// Column widths in twips. Emitted as a comma-separated string on
|
|
194
|
+
// the `<table>` element by Table.jsx; parsed back to an int array
|
|
195
|
+
// here. Used with `tableLayout: 'fixed'` to lock columns; without
|
|
196
|
+
// a fixed layout, Word redistributes columns to fit content.
|
|
197
|
+
'data-table-column-widths': {
|
|
198
|
+
path: ['tableColumnWidths'],
|
|
199
|
+
transform: parseIntList,
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
// 'fixed' | 'autofit'. Default in the adapter is 'fixed' when
|
|
203
|
+
// tableColumnWidths is set, undefined otherwise (Word's default).
|
|
204
|
+
'data-table-layout': { path: ['tableLayout'] },
|
|
205
|
+
|
|
206
|
+
// Whole-table width. Distinct from per-cell width. Same { size, type }
|
|
207
|
+
// shape (pct/dxa/auto) as cell widths.
|
|
208
|
+
'data-table-width-size': { path: ['tableWidth', 'size'] },
|
|
209
|
+
'data-table-width-type': { path: ['tableWidth', 'type'] },
|
|
210
|
+
|
|
211
|
+
// Table-level default borders. Per-cell borders still win (and
|
|
212
|
+
// already exist via `data-borders-*`); these set the table grid.
|
|
213
|
+
'data-table-borders-top-style': { path: ['tableBorders', 'top', 'style'] },
|
|
214
|
+
'data-table-borders-top-size': { path: ['tableBorders', 'top', 'size'] },
|
|
215
|
+
'data-table-borders-top-color': { path: ['tableBorders', 'top', 'color'] },
|
|
216
|
+
'data-table-borders-bottom-style': { path: ['tableBorders', 'bottom', 'style'] },
|
|
217
|
+
'data-table-borders-bottom-size': { path: ['tableBorders', 'bottom', 'size'] },
|
|
218
|
+
'data-table-borders-bottom-color': { path: ['tableBorders', 'bottom', 'color'] },
|
|
219
|
+
'data-table-borders-left-style': { path: ['tableBorders', 'left', 'style'] },
|
|
220
|
+
'data-table-borders-left-size': { path: ['tableBorders', 'left', 'size'] },
|
|
221
|
+
'data-table-borders-left-color': { path: ['tableBorders', 'left', 'color'] },
|
|
222
|
+
'data-table-borders-right-style': { path: ['tableBorders', 'right', 'style'] },
|
|
223
|
+
'data-table-borders-right-size': { path: ['tableBorders', 'right', 'size'] },
|
|
224
|
+
'data-table-borders-right-color': { path: ['tableBorders', 'right', 'color'] },
|
|
225
|
+
'data-table-borders-insideh-style': { path: ['tableBorders', 'insideHorizontal', 'style'] },
|
|
226
|
+
'data-table-borders-insideh-size': { path: ['tableBorders', 'insideHorizontal', 'size'] },
|
|
227
|
+
'data-table-borders-insideh-color': { path: ['tableBorders', 'insideHorizontal', 'color'] },
|
|
228
|
+
'data-table-borders-insidev-style': { path: ['tableBorders', 'insideVertical', 'style'] },
|
|
229
|
+
'data-table-borders-insidev-size': { path: ['tableBorders', 'insideVertical', 'size'] },
|
|
230
|
+
'data-table-borders-insidev-color': { path: ['tableBorders', 'insideVertical', 'color'] },
|
|
231
|
+
|
|
97
232
|
'data-image-type': { path: ['imageType'] },
|
|
98
233
|
|
|
99
234
|
'data-floating-horizontalposition-relative': {
|
package/src/latex/index.js
CHANGED
|
@@ -52,3 +52,8 @@ export {
|
|
|
52
52
|
// LaTeX strings that the adapter should pass through its body-text
|
|
53
53
|
// escape pass unchanged. See src/latex/raw.js for rationale.
|
|
54
54
|
export { markRawLatex, RAW_BEGIN, RAW_END } from './raw.js'
|
|
55
|
+
|
|
56
|
+
// LaTeX inset formatters — generic primitives that emit \cite{...} and
|
|
57
|
+
// \autoref{...} commands. Foundations supply the resolved keys; press
|
|
58
|
+
// supplies the emission shape + sentinel wrapping.
|
|
59
|
+
export { formatCite, formatAutoref } from './insets.js'
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LaTeX inset formatters.
|
|
3
|
+
*
|
|
4
|
+
* Generic helpers that emit `\cite{...}` and `\autoref{...}` commands
|
|
5
|
+
* wrapped in raw-passthrough sentinels (see `./raw.js`). Foundations
|
|
6
|
+
* substitute the result into paragraph text via the inline-inset
|
|
7
|
+
* resolution pass; the LaTeX adapter's body-text escape pass leaves
|
|
8
|
+
* sentinel-bracketed segments unescaped.
|
|
9
|
+
*
|
|
10
|
+
* **Resolution stays foundation-side.** These helpers do not know how
|
|
11
|
+
* to look up bibliography records or xref entries — that's a foundation
|
|
12
|
+
* concern (book uses a per-website cite-registry; @uniweb/kit/xref's
|
|
13
|
+
* registry handles xref ids). The foundation passes already-resolved
|
|
14
|
+
* key arrays in. Press's job is the LaTeX emission shape:
|
|
15
|
+
*
|
|
16
|
+
* formatCite(['darwin1859', 'mendel1866'])
|
|
17
|
+
* -> '\cite{darwin1859,mendel1866}' (sentinel-wrapped)
|
|
18
|
+
*
|
|
19
|
+
* formatCite(['darwin1859'], { locator: '42' })
|
|
20
|
+
* -> '\cite[42]{darwin1859}' (sentinel-wrapped)
|
|
21
|
+
*
|
|
22
|
+
* formatCite(['darwin1859'], { suppressAuthor: true })
|
|
23
|
+
* -> '\citeyear{darwin1859}' (sentinel-wrapped)
|
|
24
|
+
*
|
|
25
|
+
* formatAutoref(['fig-doors', 'sec-method'])
|
|
26
|
+
* -> '\autoref{fig-doors}, \autoref{sec-method}' (sentinel-wrapped)
|
|
27
|
+
*
|
|
28
|
+
* Why these live in press: foundations doing document output already
|
|
29
|
+
* import `@uniweb/press`. The shape of these helpers — emit-LaTeX with
|
|
30
|
+
* sentinel wrapping — is exactly press's domain. Adding them here lets
|
|
31
|
+
* any future academic foundation (`@uniweb/scholar`, a thesis-only
|
|
32
|
+
* variant, an arXiv-flavored foundation) reuse the LaTeX emission
|
|
33
|
+
* vocabulary without re-deriving it.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { markRawLatex } from './raw.js'
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Emit a biblatex `\cite` (or `\citeyear`) command for one or more
|
|
40
|
+
* pre-resolved bibliography keys. Returns an empty string when the
|
|
41
|
+
* caller has filtered out every key (no resolved cites → no command).
|
|
42
|
+
*
|
|
43
|
+
* @param {string[]} keys - Resolved bibliography keys. Foundation has
|
|
44
|
+
* already filtered out any keys that don't appear in the website's
|
|
45
|
+
* bibliography (biblatex would fail on unknown keys at TeX-compile
|
|
46
|
+
* time; we drop them at substitution time so authors see clean
|
|
47
|
+
* output even with typos).
|
|
48
|
+
* @param {Object} [options]
|
|
49
|
+
* @param {string} [options.locator] - Page or location reference,
|
|
50
|
+
* becomes the optional `[N]` argument: `\cite[42]{key}`.
|
|
51
|
+
* @param {boolean} [options.suppressAuthor] - When true, emit
|
|
52
|
+
* `\citeyear` instead of `\cite` (author-date styles render only the
|
|
53
|
+
* year — useful for "(Darwin 1859)" → "1859" inline).
|
|
54
|
+
* @returns {string} Sentinel-wrapped LaTeX command, or '' when keys
|
|
55
|
+
* is empty.
|
|
56
|
+
*/
|
|
57
|
+
export function formatCite(keys, options = {}) {
|
|
58
|
+
if (!Array.isArray(keys) || keys.length === 0) return ''
|
|
59
|
+
const cmd = options.suppressAuthor ? '\\citeyear' : '\\cite'
|
|
60
|
+
const list = keys.join(',')
|
|
61
|
+
const raw = options.locator
|
|
62
|
+
? `${cmd}[${String(options.locator)}]{${list}}`
|
|
63
|
+
: `${cmd}{${list}}`
|
|
64
|
+
return markRawLatex(raw)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Emit one or more `\autoref{}` commands for pre-resolved xref ids.
|
|
69
|
+
* Multi-id clusters render as comma-separated autorefs because biblatex
|
|
70
|
+
* has no native multi-cite-style \autoref grouping; cleveref does (via
|
|
71
|
+
* \cref{a,b}) but adds a package dependency the v1 LaTeX template
|
|
72
|
+
* avoids.
|
|
73
|
+
*
|
|
74
|
+
* @param {string[]} ids - Resolved xref ids (foundation filtered out
|
|
75
|
+
* anything not present in the xref-registry).
|
|
76
|
+
* @returns {string} Sentinel-wrapped LaTeX command(s), or '' when ids
|
|
77
|
+
* is empty.
|
|
78
|
+
*/
|
|
79
|
+
export function formatAutoref(ids) {
|
|
80
|
+
if (!Array.isArray(ids) || ids.length === 0) return ''
|
|
81
|
+
const refs = ids.map((id) => `\\autoref{${id}}`).join(', ')
|
|
82
|
+
return markRawLatex(refs)
|
|
83
|
+
}
|