x4js 2.0.28 → 2.0.30
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/.vscode/launch.json +14 -0
- package/.vscode/settings.json +2 -0
- package/ai-comments.txt +97 -0
- package/demo/assets/house-light.svg +1 -0
- package/demo/assets/radio.svg +4 -0
- package/demo/index.html +12 -0
- package/demo/main.scss +23 -0
- package/demo/main.ts +324 -0
- package/demo/package.json +26 -0
- package/demo/scss.d.ts +4 -0
- package/demo/svg.d.ts +1 -0
- package/demo/tsconfig.json +14 -0
- package/lib/types/x4js.d.ts +0 -2374
- package/package.json +23 -47
- package/prepack.mjs +3 -0
- package/scripts/prepack.mjs +342 -0
- package/src/colors.scss +246 -0
- package/src/components/boxes/boxes.module.scss +1 -1
- package/src/components/boxes/boxes.ts +139 -28
- package/src/components/button/button.ts +80 -33
- package/src/components/combobox/combobox.ts +1 -1
- package/src/components/dialog/dialog.ts +4 -0
- package/src/components/gridview/gridview.ts +104 -6
- package/src/components/icon/icon.ts +42 -14
- package/src/components/input/input.ts +146 -74
- package/src/components/keyboard/keyboard.module.scss +1 -1
- package/src/components/keyboard/keyboard.ts +31 -9
- package/src/components/label/label.module.scss +9 -0
- package/src/components/label/label.ts +10 -6
- package/src/components/link/link.module.scss +44 -0
- package/src/components/link/link.ts +7 -1
- package/src/components/listbox/listbox.module.scss +18 -4
- package/src/components/listbox/listbox.ts +32 -12
- package/src/components/menu/menu.module.scss +14 -2
- package/src/components/menu/menu.ts +1 -1
- package/src/components/messages/messages.ts +13 -5
- package/src/components/panel/panel.module.scss +7 -0
- package/src/components/popup/popup.ts +14 -10
- package/src/components/propgrid/propgrid.ts +1 -1
- package/src/components/shared.scss +4 -0
- package/src/components/spreadsheet/spreadsheet.ts +81 -34
- package/src/components/tabs/tabs.module.scss +1 -0
- package/src/components/textarea/textarea.ts +8 -2
- package/src/components/textedit/textedit.ts +7 -0
- package/src/components/themes.scss +2 -0
- package/src/components/tooltips/tooltips.ts +15 -3
- package/src/core/component.ts +358 -162
- package/src/core/core_application.ts +129 -32
- package/src/core/core_colors.ts +382 -119
- package/src/core/core_data.ts +73 -86
- package/src/core/core_dom.ts +10 -0
- package/src/core/core_dragdrop.ts +32 -7
- package/src/core/core_element.ts +111 -4
- package/src/core/core_events.ts +48 -11
- package/src/core/core_i18n.ts +2 -0
- package/src/core/core_pdf.ts +454 -0
- package/src/core/core_router.ts +64 -5
- package/src/core/core_state.ts +1 -0
- package/src/core/core_styles.ts +11 -12
- package/src/core/core_svg.ts +346 -51
- package/src/core/core_tools.ts +105 -17
- package/src/x4.ts +1 -0
- package/src/x4tsx.d.ts +2 -1
- package/tsconfig.json +11 -0
- package/lib/README.txt +0 -20
- package/lib/cjs/x4.css +0 -1
- package/lib/cjs/x4.js +0 -2
- package/lib/esm/x4.css +0 -1
- package/lib/esm/x4.mjs +0 -2
- package/lib/styles/x4.css +0 -1
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ___ ___ __
|
|
3
|
+
* \ \/ / / _
|
|
4
|
+
* \ / /_| |_
|
|
5
|
+
* / \____ _|
|
|
6
|
+
* /__/\__\ |_|.2
|
|
7
|
+
*
|
|
8
|
+
* @file core_pdf.ts
|
|
9
|
+
* @author Etienne Cochard
|
|
10
|
+
*
|
|
11
|
+
* @copyright (c) 2026 R-libre ingenierie
|
|
12
|
+
*
|
|
13
|
+
* Use of this source code is governed by an MIT-style license
|
|
14
|
+
* that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.
|
|
15
|
+
*
|
|
16
|
+
* taken from excellent
|
|
17
|
+
* https://github.com/Lulzx/tinypdf/tree/main
|
|
18
|
+
**/
|
|
19
|
+
|
|
20
|
+
import { Color } from './core_colors.js'
|
|
21
|
+
import { isArray, isFunction, isNumber, isPlainObject, isString } from './core_tools.js'
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
const WIDTHS: number[] = [
|
|
25
|
+
278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278,
|
|
26
|
+
556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556,
|
|
27
|
+
1015, 667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778,
|
|
28
|
+
667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 278, 278, 278, 469, 556,
|
|
29
|
+
333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500, 222, 833, 556, 556,
|
|
30
|
+
556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
export interface TextOptions {
|
|
34
|
+
align?: 'left' | 'center' | 'right'
|
|
35
|
+
width?: number
|
|
36
|
+
color?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface LinkOptions {
|
|
40
|
+
underline?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type PDFValue = null | boolean | number | string | PDFValue[] | Ref | { [key: string]: PDFValue }
|
|
44
|
+
|
|
45
|
+
interface PDFObject {
|
|
46
|
+
id: number
|
|
47
|
+
dict: Record<string, PDFValue>
|
|
48
|
+
stream: Uint8Array | null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function measureText(str: string, size: number): number {
|
|
52
|
+
let width = 0
|
|
53
|
+
for (let i = 0; i < str.length; i++) {
|
|
54
|
+
const code = str.charCodeAt(i)
|
|
55
|
+
const w = (code >= 32 && code <= 126) ? WIDTHS[code - 32] : 556
|
|
56
|
+
width += w
|
|
57
|
+
}
|
|
58
|
+
return (width * size) / 1000
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function fx( v:number, n:number ) {
|
|
62
|
+
return (+v.toFixed(n)).toString();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function formatColor( clr: Color, suffix: string ) : string {
|
|
66
|
+
const rgb = clr.toNumber( );
|
|
67
|
+
return `${fx((rgb>>16)&0xff,3)} ${fx((rgb>>8)&0xff,3)} ${fx(rgb&0xff,3)} ${suffix}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
const STRING_MAP: Record<string,string> = {
|
|
72
|
+
'\\': '\\\\',
|
|
73
|
+
'(': '\\(',
|
|
74
|
+
')': '\\)',
|
|
75
|
+
'\r': '\\r',
|
|
76
|
+
'\n': '\\n'
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function pdfString(str: string): string {
|
|
80
|
+
return '(' + str.replace( /[\\()\r\n]/g, s => STRING_MAP[s]) + ')';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function serialize(val: PDFValue): string {
|
|
84
|
+
if (val === null || val === undefined) {
|
|
85
|
+
return 'null'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if ( typeof val === 'boolean') {
|
|
89
|
+
return val ? 'true' : 'false'
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if ( isNumber( val ) ) {
|
|
93
|
+
return fx(val,4);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if ( isString(val) ) {
|
|
97
|
+
if (val.startsWith('/')) {
|
|
98
|
+
return val // name
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (val.startsWith('(')) {
|
|
102
|
+
return val // already escaped string
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return pdfString(val)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (isArray(val)) {
|
|
109
|
+
return '[' + val.map(serialize).join(' ') + ']'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (val instanceof Ref) {
|
|
113
|
+
return `${val.id} 0 R`
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if ( isPlainObject(val) ) {
|
|
117
|
+
const pairs = Object.entries(val)
|
|
118
|
+
.filter( ([_, v]) => v !== undefined)
|
|
119
|
+
.map( ([k, v]) => `/${k} ${serialize(v as PDFValue)}`);
|
|
120
|
+
|
|
121
|
+
return '<<\n' + pairs.join('\n') + '\n>>'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return String(val)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
class Ref {
|
|
128
|
+
id: number
|
|
129
|
+
constructor(id: number) { this.id = id }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
interface PageContext {
|
|
133
|
+
text(str: string, x: number, y: number, size: number, opts?: TextOptions): void;
|
|
134
|
+
rect(x: number, y: number, w: number, h: number, fill: string): void;
|
|
135
|
+
line(x1: number, y1: number, x2: number, y2: number, stroke: string, lineWidth?: number): void;
|
|
136
|
+
image(jpegBytes: Uint8Array, x: number, y: number, w: number, h: number): void;
|
|
137
|
+
link(url: string, x: number, y: number, w: number, h: number, opts?: LinkOptions): void;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export class X4PDFBuilder {
|
|
141
|
+
|
|
142
|
+
private objects: PDFObject[] = [];
|
|
143
|
+
private pages: Ref[] = [];
|
|
144
|
+
private nextId = 1;
|
|
145
|
+
|
|
146
|
+
private _addObject(dict: Record<string, PDFValue>, streamBytes: Uint8Array = null): Ref {
|
|
147
|
+
const id = this.nextId++;
|
|
148
|
+
this.objects.push({ id, dict, stream: streamBytes });
|
|
149
|
+
return new Ref(id);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
page( width: number, height: number, callback: (ctx: PageContext) => void ): void;
|
|
153
|
+
page( callback: (ctx: PageContext) => void ): void;
|
|
154
|
+
page( param1: number | ((ctx: PageContext) => void), param2?: number, param3?: (ctx: PageContext) => void ): void {
|
|
155
|
+
const ops: string[] = [];
|
|
156
|
+
const images: { name: string; ref: Ref }[] = [];
|
|
157
|
+
const links: { url: string; rect: number[] }[] = [];
|
|
158
|
+
let imageCount = 0;
|
|
159
|
+
|
|
160
|
+
const text = (str: string, x: number, y: number, size: number, opts: TextOptions = {}) => {
|
|
161
|
+
const { align = 'left', width: boxWidth, color = '#000000' } = opts;
|
|
162
|
+
|
|
163
|
+
let tx = x;
|
|
164
|
+
if (align !== 'left' && boxWidth !== undefined) {
|
|
165
|
+
const textWidth = measureText(str, size);
|
|
166
|
+
if (align === 'center') {
|
|
167
|
+
tx = x + (boxWidth - textWidth) / 2;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (align === 'right') {
|
|
171
|
+
tx = x + boxWidth - textWidth;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const rgb = new Color(color);
|
|
176
|
+
if (rgb) {
|
|
177
|
+
ops.push( formatColor(rgb, 'rg') );
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
ops.push('BT')
|
|
181
|
+
ops.push(`/F1 ${size} Tf`)
|
|
182
|
+
ops.push(`${fx(tx,2)} ${fx(y,2)} Td`)
|
|
183
|
+
ops.push(`${str.startsWith('(') ? str : pdfString(str)} Tj`)
|
|
184
|
+
ops.push('ET')
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const rect = (x: number, y: number, w: number, h: number, fill: string) => {
|
|
188
|
+
const rgb = new Color(fill)
|
|
189
|
+
if (rgb) {
|
|
190
|
+
ops.push( formatColor(rgb, 'rg' ) );
|
|
191
|
+
ops.push(`${fx(x,2)} ${fx(y,2)} ${fx(w,2)} ${fx(h,2)} re`);
|
|
192
|
+
ops.push('f');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const line = (x1: number, y1: number, x2: number, y2: number, stroke: string, lineWidth: number = 1) => {
|
|
197
|
+
const rgb = new Color(stroke)
|
|
198
|
+
if (rgb) {
|
|
199
|
+
ops.push(`${fx(lineWidth,2)} w`);
|
|
200
|
+
ops.push( formatColor( rgb,'RG') );
|
|
201
|
+
ops.push(`${fx(x1,2)} ${fx(y1,2)} m`);
|
|
202
|
+
ops.push(`${fx(x2,2)} ${fx(y2,2)} l`);
|
|
203
|
+
ops.push('S')
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const image = (jpegBytes: Uint8Array, x: number, y: number, w: number, h: number) => {
|
|
208
|
+
let imgWidth = 0, imgHeight = 0
|
|
209
|
+
for (let i = 0; i < jpegBytes.length - 1; i++) {
|
|
210
|
+
if (jpegBytes[i] === 0xFF && (jpegBytes[i + 1] === 0xC0 || jpegBytes[i + 1] === 0xC2)) {
|
|
211
|
+
imgHeight = (jpegBytes[i + 5] << 8) | jpegBytes[i + 6]
|
|
212
|
+
imgWidth = (jpegBytes[i + 7] << 8) | jpegBytes[i + 8]
|
|
213
|
+
break
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const imgName = `/Im${imageCount++}`
|
|
218
|
+
const imgRef = this._addObject({
|
|
219
|
+
Type: '/XObject',
|
|
220
|
+
Subtype: '/Image',
|
|
221
|
+
Width: imgWidth,
|
|
222
|
+
Height: imgHeight,
|
|
223
|
+
ColorSpace: '/DeviceRGB',
|
|
224
|
+
BitsPerComponent: 8,
|
|
225
|
+
Filter: '/DCTDecode',
|
|
226
|
+
Length: jpegBytes.length
|
|
227
|
+
}, jpegBytes)
|
|
228
|
+
|
|
229
|
+
images.push({ name: imgName, ref: imgRef })
|
|
230
|
+
|
|
231
|
+
ops.push('q')
|
|
232
|
+
ops.push(`${fx(w,2)} 0 0 ${fx(h,2)} ${fx(x,2)} ${fx(y,2)} cm`)
|
|
233
|
+
ops.push(`${imgName} Do`)
|
|
234
|
+
ops.push('Q')
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const link = (url: string, x: number, y: number, w: number, h: number, opts: LinkOptions = {}) => {
|
|
238
|
+
links.push({ url, rect: [x, y, x + w, y + h] })
|
|
239
|
+
if (opts.underline) {
|
|
240
|
+
const rgb = new Color(opts.underline)
|
|
241
|
+
if (rgb) {
|
|
242
|
+
ops.push(`0.75 w`)
|
|
243
|
+
ops.push( formatColor(rgb, 'RG' ) );
|
|
244
|
+
ops.push(`${fx(x,2)} ${fx(y + 2,2)} m`);
|
|
245
|
+
ops.push(`${fx(x + w,2)} ${fx(y + 2,2)} l`);
|
|
246
|
+
ops.push('S')
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const ctx: PageContext = {
|
|
252
|
+
text,
|
|
253
|
+
rect,
|
|
254
|
+
line,
|
|
255
|
+
image,
|
|
256
|
+
link
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let width: number
|
|
260
|
+
let height: number
|
|
261
|
+
|
|
262
|
+
if ( isFunction( param1) ) {
|
|
263
|
+
width = 612
|
|
264
|
+
height = 792
|
|
265
|
+
param1( ctx );
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
width = param1;
|
|
269
|
+
height = param2;
|
|
270
|
+
param3( ctx );
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const content = ops.join('\n')
|
|
274
|
+
const contentBytes = new TextEncoder().encode(content)
|
|
275
|
+
|
|
276
|
+
const contentRef = this._addObject({ Length: contentBytes.length }, contentBytes)
|
|
277
|
+
|
|
278
|
+
const xobjects: Record<string, Ref> = {}
|
|
279
|
+
for (const img of images) {
|
|
280
|
+
xobjects[img.name.slice(1)] = img.ref
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const annots: Ref[] = links.map(lnk => this._addObject({
|
|
284
|
+
Type: '/Annot',
|
|
285
|
+
Subtype: '/Link',
|
|
286
|
+
Rect: lnk.rect,
|
|
287
|
+
Border: [0, 0, 0],
|
|
288
|
+
A: { Type: '/Action', S: '/URI', URI: lnk.url }
|
|
289
|
+
}, null ) )
|
|
290
|
+
|
|
291
|
+
const pageRef = this._addObject({
|
|
292
|
+
Type: '/Page',
|
|
293
|
+
Parent: null,
|
|
294
|
+
MediaBox: [0, 0, width, height],
|
|
295
|
+
Contents: contentRef,
|
|
296
|
+
Resources: {
|
|
297
|
+
Font: { F1: null },
|
|
298
|
+
XObject: Object.keys(xobjects).length > 0 ? xobjects : undefined
|
|
299
|
+
},
|
|
300
|
+
Annots: annots.length > 0 ? annots : undefined
|
|
301
|
+
}, null )
|
|
302
|
+
|
|
303
|
+
this.pages.push(pageRef)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
build(): Uint8Array {
|
|
307
|
+
const fontRef = this._addObject({
|
|
308
|
+
Type: '/Font',
|
|
309
|
+
Subtype: '/Type1',
|
|
310
|
+
BaseFont: '/Helvetica',
|
|
311
|
+
Encoding: '/WinAnsiEncoding'
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
const pagesRef = this._addObject({
|
|
315
|
+
Type: '/Pages',
|
|
316
|
+
Kids: this.pages,
|
|
317
|
+
Count: this.pages.length
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
for (const obj of this.objects) {
|
|
321
|
+
if (obj.dict.Type === '/Page') {
|
|
322
|
+
obj.dict.Parent = pagesRef
|
|
323
|
+
const resources = obj.dict.Resources as Record<string, PDFValue> | undefined
|
|
324
|
+
if (resources?.Font) {
|
|
325
|
+
(resources.Font as Record<string, PDFValue>).F1 = fontRef
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const catalogRef = this._addObject({
|
|
331
|
+
Type: '/Catalog',
|
|
332
|
+
Pages: pagesRef
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
const parts: (string | Uint8Array)[] = []
|
|
336
|
+
const offsets: number[] = []
|
|
337
|
+
|
|
338
|
+
parts.push('%PDF-1.4\n%\xFF\xFF\xFF\xFF\n')
|
|
339
|
+
|
|
340
|
+
for (const obj of this.objects) {
|
|
341
|
+
offsets[obj.id] = parts.reduce((sum, p) => sum + (typeof p === 'string' ? new TextEncoder().encode(p).length : p.length), 0)
|
|
342
|
+
|
|
343
|
+
let content = `${obj.id} 0 obj\n${serialize(obj.dict)}\n`
|
|
344
|
+
if (obj.stream) {
|
|
345
|
+
content += 'stream\n'
|
|
346
|
+
parts.push(content)
|
|
347
|
+
parts.push(obj.stream)
|
|
348
|
+
parts.push('\nendstream\nendobj\n')
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
content += 'endobj\n'
|
|
352
|
+
parts.push(content)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const xrefOffset = parts.reduce((sum, p) => sum + (typeof p === 'string' ? new TextEncoder().encode(p).length : p.length), 0)
|
|
357
|
+
|
|
358
|
+
let xref = `xref\n0 ${this.objects.length + 1}\n`
|
|
359
|
+
xref += '0000000000 65535 f \n'
|
|
360
|
+
for (let i = 1; i <= this.objects.length; i++) {
|
|
361
|
+
xref += String(offsets[i]).padStart(10, '0') + ' 00000 n \n'
|
|
362
|
+
}
|
|
363
|
+
parts.push(xref)
|
|
364
|
+
|
|
365
|
+
parts.push(`trailer\n${serialize({ Size: this.objects.length + 1, Root: catalogRef })}\n`)
|
|
366
|
+
parts.push(`startxref\n${xrefOffset}\n%%EOF\n`)
|
|
367
|
+
|
|
368
|
+
const totalLength = parts.reduce((sum, p) => sum + (typeof p === 'string' ? new TextEncoder().encode(p).length : p.length), 0)
|
|
369
|
+
const result = new Uint8Array(totalLength)
|
|
370
|
+
let offset = 0
|
|
371
|
+
for (const part of parts) {
|
|
372
|
+
const bytes = typeof part === 'string' ? new TextEncoder().encode(part) : part
|
|
373
|
+
result.set(bytes, offset)
|
|
374
|
+
offset += bytes.length
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return result
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/*
|
|
382
|
+
export function markdown(md: string, opts: { width?: number; height?: number; margin?: number } = {}): Uint8Array {
|
|
383
|
+
const W = opts.width ?? 612, H = opts.height ?? 792, M = opts.margin ?? 72
|
|
384
|
+
const doc = pdf(), textW = W - M * 2, bodySize = 11, lineH = bodySize * 1.5
|
|
385
|
+
type Item = { text: string; size: number; indent: number; spaceBefore: number; spaceAfter: number; rule?: boolean; color?: string }
|
|
386
|
+
const items: Item[] = []
|
|
387
|
+
|
|
388
|
+
const wrap = (text: string, size: number, maxW: number): string[] => {
|
|
389
|
+
const words = text.split(' '), lines: string[] = []
|
|
390
|
+
let line = ''
|
|
391
|
+
for (const word of words) {
|
|
392
|
+
const test = line ? line + ' ' + word : word
|
|
393
|
+
if (measureText(test, size) <= maxW) line = test
|
|
394
|
+
else { if (line) lines.push(line); line = word }
|
|
395
|
+
}
|
|
396
|
+
if (line) lines.push(line)
|
|
397
|
+
return lines.length ? lines : ['']
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
let prevType = 'start'
|
|
401
|
+
for (const raw of md.split('\n')) {
|
|
402
|
+
const line = raw.trimEnd()
|
|
403
|
+
if (/^#{1,3}\s/.test(line)) {
|
|
404
|
+
const lvl = line.match(/^#+/)![0].length
|
|
405
|
+
const size = [22, 16, 13][lvl - 1]
|
|
406
|
+
const before = prevType === 'start' ? 0 : [14, 12, 10][lvl - 1]
|
|
407
|
+
const wrapped = wrap(line.slice(lvl + 1), size, textW)
|
|
408
|
+
wrapped.forEach((l, i) => items.push({ text: l, size, indent: 0, spaceBefore: i === 0 ? before : 0, spaceAfter: 4, color: '#111111' }))
|
|
409
|
+
prevType = 'header'
|
|
410
|
+
} else if (/^[-*]\s/.test(line)) {
|
|
411
|
+
const wrapped = wrap(line.slice(2), bodySize, textW - 18)
|
|
412
|
+
wrapped.forEach((l, i) => items.push({ text: (i === 0 ? '- ' : ' ') + l, size: bodySize, indent: 12, spaceBefore: 0, spaceAfter: 2 }))
|
|
413
|
+
prevType = 'list'
|
|
414
|
+
} else if (/^\d+\.\s/.test(line)) {
|
|
415
|
+
const num = line.match(/^\d+/)![0]
|
|
416
|
+
const text = line.slice(num.length + 2)
|
|
417
|
+
const wrapped = wrap(text, bodySize, textW - 18)
|
|
418
|
+
wrapped.forEach((l, i) => items.push({ text: (i === 0 ? num + '. ' : ' ') + l, size: bodySize, indent: 12, spaceBefore: 0, spaceAfter: 2 }))
|
|
419
|
+
prevType = 'list'
|
|
420
|
+
} else if (/^(-{3,}|\*{3,}|_{3,})$/.test(line)) {
|
|
421
|
+
items.push({ text: '', size: bodySize, indent: 0, spaceBefore: 8, spaceAfter: 8, rule: true })
|
|
422
|
+
prevType = 'rule'
|
|
423
|
+
} else if (line.trim() === '') {
|
|
424
|
+
if (prevType !== 'start' && prevType !== 'blank') items.push({ text: '', size: bodySize, indent: 0, spaceBefore: 0, spaceAfter: 4 })
|
|
425
|
+
prevType = 'blank'
|
|
426
|
+
} else {
|
|
427
|
+
const wrapped = wrap(line, bodySize, textW)
|
|
428
|
+
wrapped.forEach((l, i) => items.push({ text: l, size: bodySize, indent: 0, spaceBefore: 0, spaceAfter: 4, color: '#111111' }))
|
|
429
|
+
prevType = 'para'
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const pages: { items: Item[]; ys: number[] }[] = []
|
|
434
|
+
let y = H - M, pg: Item[] = [], ys: number[] = []
|
|
435
|
+
for (const item of items) {
|
|
436
|
+
const needed = item.spaceBefore + item.size + item.spaceAfter
|
|
437
|
+
if (y - needed < M) { pages.push({ items: pg, ys }); pg = []; ys = []; y = H - M }
|
|
438
|
+
y -= item.spaceBefore
|
|
439
|
+
ys.push(y); pg.push(item)
|
|
440
|
+
y -= item.size + item.spaceAfter
|
|
441
|
+
}
|
|
442
|
+
if (pg.length) pages.push({ items: pg, ys })
|
|
443
|
+
|
|
444
|
+
for (const { items: pi, ys: py } of pages) {
|
|
445
|
+
doc.page(W, H, ctx => {
|
|
446
|
+
pi.forEach((it, i) => {
|
|
447
|
+
if (it.rule) ctx.line(M, py[i], W - M, py[i], '#e0e0e0', 0.5)
|
|
448
|
+
else if (it.text) ctx.text(it.text, M + it.indent, py[i], it.size, { color: it.color })
|
|
449
|
+
})
|
|
450
|
+
})
|
|
451
|
+
}
|
|
452
|
+
return doc.build()
|
|
453
|
+
}
|
|
454
|
+
*/
|
package/src/core/core_router.ts
CHANGED
|
@@ -20,16 +20,32 @@ import { EventMap, EventSource } from './core_events';
|
|
|
20
20
|
type RouteHandler = ( params: any, path: string ) => void;
|
|
21
21
|
|
|
22
22
|
interface Segment {
|
|
23
|
-
|
|
23
|
+
/** The names of the parameters extracted from the route string (e.g., `['id']` for `'/detail/:id'`). */
|
|
24
|
+
keys: string[];
|
|
25
|
+
/** The regular expression used to match the route against a URL path. */
|
|
24
26
|
pattern: RegExp;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
interface Route {
|
|
28
|
-
|
|
30
|
+
/** The names of the parameters extracted from the route string. */
|
|
31
|
+
keys: string[];
|
|
32
|
+
/** The regular expression used to match the route against a URL path. */
|
|
29
33
|
pattern: RegExp;
|
|
34
|
+
/** The function to call when this route matches the current URL. */
|
|
30
35
|
handler: RouteHandler;
|
|
31
36
|
}
|
|
32
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Parses a route string or regular expression into a `Segment` object.
|
|
40
|
+
* This function converts human-readable route patterns (e.g., `'/users/:id'`) into
|
|
41
|
+
* a regular expression that can be used for matching URL paths, and extracts parameter names.
|
|
42
|
+
*
|
|
43
|
+
* @param str - The route pattern as a string (e.g., `'/users/:id'`) or a direct `RegExp` object.
|
|
44
|
+
* @param loose - If `true`, the pattern will match paths that start with the route but may have additional segments.
|
|
45
|
+
* If `false` (default), the pattern must match the entire path.
|
|
46
|
+
* @returns A `Segment` object containing the extracted keys and the compiled regular expression.
|
|
47
|
+
*/
|
|
48
|
+
|
|
33
49
|
export function parseRoute(str: string | RegExp, loose = false): Segment {
|
|
34
50
|
|
|
35
51
|
if (str instanceof RegExp) {
|
|
@@ -76,8 +92,14 @@ export function parseRoute(str: string | RegExp, loose = false): Segment {
|
|
|
76
92
|
};
|
|
77
93
|
}
|
|
78
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Defines the events that the `Router` can emit.
|
|
97
|
+
*/
|
|
98
|
+
|
|
79
99
|
interface RouterEvents extends EventMap {
|
|
100
|
+
/** Emitted when the route changes successfully. The `value` property contains the new URL path. */
|
|
80
101
|
change: EvChange;
|
|
102
|
+
/** Emitted when a route is not found (404 error). The `code` is 404 and `message` describes the error. */
|
|
81
103
|
error: EvError;
|
|
82
104
|
}
|
|
83
105
|
|
|
@@ -130,11 +152,26 @@ export class Router extends EventSource< RouterEvents > {
|
|
|
130
152
|
});
|
|
131
153
|
}
|
|
132
154
|
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Registers a route handler for the given URI pattern.
|
|
158
|
+
*
|
|
159
|
+
* @param uri - A path string (may include parameter placeholders) or a RegExp used to match request URIs.
|
|
160
|
+
* @param handler - A function conforming to the RouteHandler type that will be executed when a request matches the route.
|
|
161
|
+
*
|
|
162
|
+
* @remarks
|
|
163
|
+
* The provided uri is parsed (via parseRoute) into a RegExp pattern and an ordered list of parameter keys.
|
|
164
|
+
* The resulting route descriptor ({ keys, pattern, handler }) is appended to the router's internal route list.
|
|
165
|
+
*
|
|
166
|
+
* @returns void
|
|
167
|
+
*/
|
|
168
|
+
|
|
133
169
|
get(uri: string | RegExp, handler: RouteHandler ) {
|
|
134
170
|
let { keys, pattern } = parseRoute(uri);
|
|
135
171
|
this.m_routes.push({ keys, pattern, handler });
|
|
136
172
|
}
|
|
137
173
|
|
|
174
|
+
|
|
138
175
|
init() {
|
|
139
176
|
this.navigate( this._getLocation() );
|
|
140
177
|
}
|
|
@@ -144,9 +181,32 @@ export class Router extends EventSource< RouterEvents > {
|
|
|
144
181
|
}
|
|
145
182
|
|
|
146
183
|
/**
|
|
147
|
-
*
|
|
184
|
+
* Navigate to a given URI within the router, update browser history, and optionally notify route handlers.
|
|
185
|
+
*
|
|
186
|
+
* Normalizes the supplied URI
|
|
187
|
+
* updates the browser history and emits lifecycle events.
|
|
188
|
+
*
|
|
189
|
+
* Behavior summary:
|
|
190
|
+
* - If no matching route or no handlers are found, logs a message, fires an "error" event with
|
|
191
|
+
* { code: 404, message: "route not found" } and returns false.
|
|
192
|
+
* - If notify is true, invokes the first handler of the matched route with (params, uri).
|
|
193
|
+
* - Always fires a "change" event with { value: this._getLocation() } after performing the navigation.
|
|
194
|
+
*
|
|
195
|
+
* @param uri - Target URI to navigate to. If it does not start with '/', a leading '/' will be added.
|
|
196
|
+
* When m_useHash is true the resulting location will be converted to a '#...' fragment.
|
|
197
|
+
* @param notify - Whether to invoke the matched route handler after updating history. Defaults to true.
|
|
198
|
+
* @param replace - Whether to replace the current history entry (true) or push a new one (false). Defaults to false.
|
|
199
|
+
* @returns True if navigation succeeded (route found and history updated); false if no route or handlers were found.
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* // Navigate to /dashboard and notify handlers (pushes new history entry)
|
|
203
|
+
* navigate('/dashboard');
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* // Replace the current history entry without notifying handlers
|
|
207
|
+
* navigate('/login', false, true);
|
|
148
208
|
*/
|
|
149
|
-
|
|
209
|
+
|
|
150
210
|
navigate( uri: string, notify = true, replace = false ) {
|
|
151
211
|
|
|
152
212
|
if( !uri.startsWith('/') ) {
|
|
@@ -234,4 +294,3 @@ export class Router extends EventSource< RouterEvents > {
|
|
|
234
294
|
return { params, handlers };
|
|
235
295
|
}
|
|
236
296
|
}
|
|
237
|
-
|
package/src/core/core_state.ts
CHANGED
package/src/core/core_styles.ts
CHANGED
|
@@ -82,11 +82,11 @@ export function isUnitLess( name: string ) {
|
|
|
82
82
|
|
|
83
83
|
export class Stylesheet {
|
|
84
84
|
|
|
85
|
-
readonly
|
|
86
|
-
readonly
|
|
85
|
+
readonly _sheet: CSSStyleSheet;
|
|
86
|
+
readonly _rules: Map<string, number> = new Map( );
|
|
87
87
|
|
|
88
88
|
constructor() {
|
|
89
|
-
|
|
89
|
+
|
|
90
90
|
function getStyleSheet( name: string ) : CSSStyleSheet {
|
|
91
91
|
for(let i=0; i<document.styleSheets.length; i++) {
|
|
92
92
|
let sheet = document.styleSheets[i];
|
|
@@ -96,12 +96,12 @@ export class Stylesheet {
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
this.
|
|
100
|
-
if( !this.
|
|
99
|
+
this._sheet = getStyleSheet( 'x4-dynamic-css' );
|
|
100
|
+
if( !this._sheet ) {
|
|
101
101
|
const dom = document.createElement( 'style' );
|
|
102
102
|
dom.setAttribute('id', 'x4-dynamic-css' );
|
|
103
103
|
document.head.appendChild(dom);
|
|
104
|
-
this.
|
|
104
|
+
this._sheet = dom.sheet
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
@@ -116,15 +116,15 @@ export class Stylesheet {
|
|
|
116
116
|
public setRule(name: string, definition: any ) {
|
|
117
117
|
|
|
118
118
|
if( isString(definition) ) {
|
|
119
|
-
let index = this.
|
|
119
|
+
let index = this._rules.get( name );
|
|
120
120
|
if (index !== undefined) {
|
|
121
|
-
this.
|
|
121
|
+
this._sheet.deleteRule(index);
|
|
122
122
|
}
|
|
123
123
|
else {
|
|
124
|
-
index = this.
|
|
124
|
+
index = this._sheet.cssRules.length;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
this.
|
|
127
|
+
this._rules.set( name, this._sheet.insertRule( definition, index) );
|
|
128
128
|
}
|
|
129
129
|
else {
|
|
130
130
|
let idx = 1;
|
|
@@ -152,7 +152,7 @@ export class Stylesheet {
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
/**
|
|
155
|
-
*
|
|
155
|
+
* Returns the computed value of a CSS custom property (CSS variable) from the document's root element.
|
|
156
156
|
* @param name - variable name
|
|
157
157
|
* @example
|
|
158
158
|
* ```
|
|
@@ -211,4 +211,3 @@ export class ComputedStyle {
|
|
|
211
211
|
return this.m_style;
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
|
-
|