@quillmark/quillify 0.3.1 → 0.5.0
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/NOTICE +1 -1
- package/dist/api.d.ts +1 -0
- package/dist/assemble.d.ts +1 -1
- package/dist/assemble.js +12 -26
- package/dist/display-list.js +10 -4
- package/dist/naming.js +2 -0
- package/dist/quillify.js +2 -2
- package/dist/types.d.ts +2 -3
- package/dist/xfa-widgets.d.ts +1 -1
- package/dist/xfa.js +28 -3
- package/package.json +3 -3
package/NOTICE
CHANGED
package/dist/api.d.ts
CHANGED
package/dist/assemble.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { InventoryWidget, QualifiedField, QualifiedForm, QuillTree } from './types.js';
|
|
2
|
-
/** What the assembly reads of a field, of the form and of a widget. Its tests hand it a title, which it does not read. */
|
|
2
|
+
/** What the assembly reads of a field, of the form and of a widget. Its tests hand it a quill title, which it does not read. */
|
|
3
3
|
type Named = Omit<QualifiedField, 'label'>;
|
|
4
4
|
type Qualified = {
|
|
5
5
|
quill: Pick<QualifiedForm['quill'], 'name' | 'description'> & {
|
package/dist/assemble.js
CHANGED
|
@@ -1,35 +1,27 @@
|
|
|
1
1
|
/** Every quill the library builds is a first version. */
|
|
2
2
|
export const QUILL_VERSION = '0.1.0';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* (U+007F to U+009F, U+FFFE and U+FFFF) are written as `\u` escapes.
|
|
4
|
+
* JSON as YAML the engine reads: the characters its reader refuses and JSON
|
|
5
|
+
* leaves raw (U+007F to U+009F, U+FFFE and U+FFFF) are written as `\u` escapes.
|
|
7
6
|
*/
|
|
8
|
-
const
|
|
9
|
-
/** A
|
|
10
|
-
|
|
11
|
-
const id = name
|
|
12
|
-
.toLowerCase()
|
|
13
|
-
.replace(/[^a-z0-9]+/g, '_')
|
|
14
|
-
.replace(/^_+|_+$/g, '');
|
|
15
|
-
return /^[a-z]/.test(id) ? id : `group_${id}`;
|
|
16
|
-
}
|
|
7
|
+
const escaped = (json) => json.replace(/[\u007f-\u009f\ufffe\uffff]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
8
|
+
/** A string as a YAML double-quoted scalar, which JSON writes from a well formed string. */
|
|
9
|
+
const q = (s) => escaped(JSON.stringify(s.toWellFormed()));
|
|
17
10
|
/**
|
|
18
11
|
* A `default:` says nobody need answer the field. A required field declares
|
|
19
12
|
* none, a boolean apart: it defaults to false either way.
|
|
20
13
|
*/
|
|
21
14
|
function fieldYaml(field) {
|
|
22
15
|
const lines = [` ${q(field.name)}:`];
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
ui.push(` group: ${q(groupId(field.group))}`);
|
|
16
|
+
if (field.title !== undefined)
|
|
17
|
+
lines.push(` title: ${q(field.title)}`);
|
|
26
18
|
switch (field.type) {
|
|
27
19
|
case 'string':
|
|
28
|
-
lines.push(' type:
|
|
20
|
+
lines.push(' type: text');
|
|
29
21
|
if (!field.required)
|
|
30
22
|
lines.push(' default: ""');
|
|
31
23
|
if (field.multiline)
|
|
32
|
-
|
|
24
|
+
lines.push(' multiline: true');
|
|
33
25
|
break;
|
|
34
26
|
case 'boolean':
|
|
35
27
|
lines.push(' type: boolean', ' default: false');
|
|
@@ -40,7 +32,7 @@ function fieldYaml(field) {
|
|
|
40
32
|
// It refuses ";", ">" and "|" in a value too, so a dropdown with one is a text box.
|
|
41
33
|
const values = (field.options ?? []).filter((option) => option !== '');
|
|
42
34
|
if (values.length === 0 || values.some((value) => /[;>|]/.test(value)))
|
|
43
|
-
lines.push(' type:
|
|
35
|
+
lines.push(' type: text');
|
|
44
36
|
else {
|
|
45
37
|
lines.push(' type: enum', ' values:');
|
|
46
38
|
for (const value of values)
|
|
@@ -51,14 +43,11 @@ function fieldYaml(field) {
|
|
|
51
43
|
break;
|
|
52
44
|
}
|
|
53
45
|
case 'array':
|
|
54
|
-
lines.push(' type: array', ' items:', ' type:
|
|
46
|
+
lines.push(' type: array', ' items:', ' type: text');
|
|
55
47
|
if (!field.required)
|
|
56
48
|
lines.push(' default: []');
|
|
57
|
-
ui.push(' multiline: true');
|
|
58
49
|
break;
|
|
59
50
|
}
|
|
60
|
-
if (ui.length > 0)
|
|
61
|
-
lines.push(' ui:', ...ui);
|
|
62
51
|
lines.push(` description: ${q(field.description)}`);
|
|
63
52
|
return lines;
|
|
64
53
|
}
|
|
@@ -74,9 +63,6 @@ export function quillYaml(form, fields) {
|
|
|
74
63
|
' body:',
|
|
75
64
|
' enabled: false'
|
|
76
65
|
];
|
|
77
|
-
const groups = [...new Set(fields.flatMap((f) => (f.group ? [groupId(f.group)] : [])))];
|
|
78
|
-
if (groups.length > 0)
|
|
79
|
-
lines.push(' ui:', ` groups: [${groups.map(q).join(', ')}]`);
|
|
80
66
|
if (fields.length > 0) {
|
|
81
67
|
lines.push(' fields:');
|
|
82
68
|
for (const field of fields)
|
|
@@ -142,7 +128,7 @@ export function assembleQuill(form, inventory, formPdf) {
|
|
|
142
128
|
const tree = new Map([
|
|
143
129
|
['Quill.yaml', encoder.encode(quillYaml(form, bound))],
|
|
144
130
|
['form.pdf', formPdf],
|
|
145
|
-
['form.
|
|
131
|
+
['form.yaml', encoder.encode(escaped(JSON.stringify(formJson, null, 2)) + '\n')]
|
|
146
132
|
]);
|
|
147
133
|
const example = exampleMarkdown(form, bound);
|
|
148
134
|
if (example)
|
package/dist/display-list.js
CHANGED
|
@@ -274,12 +274,13 @@ function svgOnly(tag) {
|
|
|
274
274
|
* One run per visual line, with the box the browser measured it into. Splitting
|
|
275
275
|
* where the rect top changes is what keeps a wrapped paragraph's second line at
|
|
276
276
|
* its own baseline. A tab ends a run too and is in none: the text after it
|
|
277
|
-
* starts a run at the box the layout moved it to.
|
|
277
|
+
* starts a run at the box the layout moved it to. A soft hyphen the layout
|
|
278
|
+
* breaks a line at is a hyphen in the run it ends, which is measured with it.
|
|
278
279
|
*/
|
|
279
280
|
function collectText(node, X, Y, drawn, turns) {
|
|
280
|
-
|
|
281
|
+
let text = node.nodeValue ?? '';
|
|
281
282
|
const parent = node.parentElement;
|
|
282
|
-
if (!text
|
|
283
|
+
if (!text.trim() || !parent)
|
|
283
284
|
return;
|
|
284
285
|
// A <select> is de-chromed above; only the option the form shows is drawn.
|
|
285
286
|
if (parent.tagName === 'OPTION' && !parent.selected)
|
|
@@ -347,10 +348,15 @@ function collectText(node, X, Y, drawn, turns) {
|
|
|
347
348
|
}
|
|
348
349
|
range.setStart(node, i);
|
|
349
350
|
range.setEnd(node, i + 1);
|
|
350
|
-
|
|
351
|
+
// At a break the soft hyphen has an empty rect and then the box of the
|
|
352
|
+
// hyphen shown, and the letter after it has that box and then its own.
|
|
353
|
+
const rects = range.getClientRects();
|
|
354
|
+
const rect = rects[rects.length - 1];
|
|
351
355
|
if (!rect)
|
|
352
356
|
continue;
|
|
353
357
|
const glyph = upright(rect);
|
|
358
|
+
if (text[i] === '\u00ad' && glyph.right > glyph.x)
|
|
359
|
+
text = `${text.slice(0, i)}-${text.slice(i + 1)}`;
|
|
354
360
|
if (top === null || Math.abs(glyph.top - top) < 0.6) {
|
|
355
361
|
top ??= glyph.top;
|
|
356
362
|
box = box
|
package/dist/naming.js
CHANGED
|
@@ -74,6 +74,8 @@ function shaped({ name, description, fields }) {
|
|
|
74
74
|
not(`${slot}.name`, 'a string');
|
|
75
75
|
if (typeof entry.description !== 'string')
|
|
76
76
|
not(`${slot}.description`, 'a string');
|
|
77
|
+
if (entry.title !== undefined && typeof entry.title !== 'string')
|
|
78
|
+
not(`${slot}.title`, 'a string');
|
|
77
79
|
if (entry.example !== undefined && typeof entry.example !== 'string')
|
|
78
80
|
not(`${slot}.example`, 'a string');
|
|
79
81
|
if (entry.text !== undefined && !TEXT.has(entry.text))
|
package/dist/quillify.js
CHANGED
|
@@ -50,12 +50,12 @@ export async function quillify(form, naming) {
|
|
|
50
50
|
return {
|
|
51
51
|
widget: entry.id,
|
|
52
52
|
name: entry.name,
|
|
53
|
+
...(entry.title === undefined ? {} : { title: entry.title }),
|
|
53
54
|
description: entry.description,
|
|
54
55
|
...typed(field, entry.text),
|
|
55
56
|
...(example === undefined ? {} : { example }),
|
|
56
57
|
keep: entry.keep && !field.readOnly,
|
|
57
|
-
required: field.required
|
|
58
|
-
...(field.kind === 'radio' ? { group: field.group } : {})
|
|
58
|
+
required: field.required
|
|
59
59
|
};
|
|
60
60
|
})
|
|
61
61
|
}, {
|
package/dist/types.d.ts
CHANGED
|
@@ -62,6 +62,7 @@ export interface QualifiedField {
|
|
|
62
62
|
/** snake_case schema field name */
|
|
63
63
|
name: string;
|
|
64
64
|
label: string;
|
|
65
|
+
title?: string;
|
|
65
66
|
description: string;
|
|
66
67
|
type: FieldType;
|
|
67
68
|
multiline: boolean;
|
|
@@ -70,8 +71,6 @@ export interface QualifiedField {
|
|
|
70
71
|
keep: boolean;
|
|
71
72
|
/** `/Ff` bit 2: the form says this one has to be filled. */
|
|
72
73
|
required: boolean;
|
|
73
|
-
/** The radio group this option belongs to, when it is one. */
|
|
74
|
-
group?: string;
|
|
75
74
|
}
|
|
76
75
|
export interface QualifiedForm {
|
|
77
76
|
quill: {
|
|
@@ -82,7 +81,7 @@ export interface QualifiedForm {
|
|
|
82
81
|
fields: QualifiedField[];
|
|
83
82
|
}
|
|
84
83
|
export declare const SNAKE_CASE: RegExp;
|
|
85
|
-
/** A quill as files, the shape `Quill.fromTree`
|
|
84
|
+
/** A quill as files, the shape `Quill.fromTree` takes. */
|
|
86
85
|
export type QuillTree = Map<string, Uint8Array>;
|
|
87
86
|
/** The largest file `sanitize` opens. It bounds the compressed input. */
|
|
88
87
|
export declare const MAX_BYTES: number;
|
package/dist/xfa-widgets.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export interface XfaWidget {
|
|
|
8
8
|
name: string;
|
|
9
9
|
kind: XfaKind;
|
|
10
10
|
page: number;
|
|
11
|
-
/** Points, top-left origin, page-relative: the rect `form.
|
|
11
|
+
/** Points, top-left origin, page-relative: the rect `form.yaml` wants. */
|
|
12
12
|
x: number;
|
|
13
13
|
y: number;
|
|
14
14
|
w: number;
|
package/dist/xfa.js
CHANGED
|
@@ -54,9 +54,11 @@ export async function xfaToAcroForm(bytes, signal) {
|
|
|
54
54
|
construct: 'image',
|
|
55
55
|
detail: 'pixels'
|
|
56
56
|
});
|
|
57
|
-
|
|
57
|
+
const harvested = harvestWidgets(laid, number);
|
|
58
|
+
for (const widget of harvested)
|
|
58
59
|
widgets.push(widget);
|
|
59
|
-
|
|
60
|
+
const shown = whiten(list, harvested);
|
|
61
|
+
await paint(out.addPage([list.width, list.height]), number, shown, async (name) => {
|
|
60
62
|
let font = fonts.get(name);
|
|
61
63
|
if (!font)
|
|
62
64
|
fonts.set(name, (font = await out.embedFont(name)));
|
|
@@ -71,6 +73,28 @@ export async function xfaToAcroForm(bytes, signal) {
|
|
|
71
73
|
await opened?.close();
|
|
72
74
|
}
|
|
73
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* The engine fills every field in black, whatever colour the form gives its
|
|
78
|
+
* text. A field whose middle lies on a fill that black reads on at under 4.5:1,
|
|
79
|
+
* WCAG's floor for text, gets a white box drawn just over that fill.
|
|
80
|
+
*/
|
|
81
|
+
function whiten(list, widgets) {
|
|
82
|
+
const drawn = [...list.drawn];
|
|
83
|
+
const filled = (item) => 'edges' in item && item.fill !== null;
|
|
84
|
+
for (const { x, y, w, h } of widgets) {
|
|
85
|
+
const [u, v] = [x + w / 2, y + h / 2];
|
|
86
|
+
const at = drawn.findLastIndex((item) => filled(item) && item.x <= u && u <= item.x + item.w && item.y <= v && v <= item.y + item.h);
|
|
87
|
+
const under = drawn[at];
|
|
88
|
+
if (at >= 0 && filled(under) && (luminance(under.fill) + 0.05) / 0.05 < 4.5)
|
|
89
|
+
drawn.splice(at + 1, 0, { x, y, w, h, fill: [1, 1, 1], edges: [null, null, null, null] });
|
|
90
|
+
}
|
|
91
|
+
return { ...list, drawn };
|
|
92
|
+
}
|
|
93
|
+
/** The relative luminance WCAG gives a colour in sRGB. */
|
|
94
|
+
function luminance(colour) {
|
|
95
|
+
const [r, g, b] = colour.map((c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4));
|
|
96
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
97
|
+
}
|
|
74
98
|
/** PDF's origin is bottom-left; every primitive arrives top-left. */
|
|
75
99
|
async function paint(page, pageNumber, list, font) {
|
|
76
100
|
const H = list.height;
|
|
@@ -184,7 +208,8 @@ async function drawRun(page, pageNumber, run, H, get) {
|
|
|
184
208
|
// The standard-14 text fonts encode WinAnsi alone. U+2010 and U+2011 are
|
|
185
209
|
// drawn as its hyphen, and any other character outside it in the first of
|
|
186
210
|
// SYMBOLS that holds it. A soft hyphen is left out: WinAnsi has it as a
|
|
187
|
-
// hyphen, and the browser lays it out at no width and shows nothing.
|
|
211
|
+
// hyphen, and the browser lays it out at no width and shows nothing. The
|
|
212
|
+
// one it breaks a line at is a hyphen in the run already.
|
|
188
213
|
// Dropping the run would lose a line of the form without anything
|
|
189
214
|
// reporting it.
|
|
190
215
|
// pdf-lib kerns the width it measures and writes no kerning, so the width
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quillmark/quillify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Turn a published PDF form (AcroForm or XFA) into a Quillmark quill, in the browser.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@neslinesli93/qpdf-wasm": "0.3.0",
|
|
51
|
-
"@quillmark/wasm": "0.
|
|
51
|
+
"@quillmark/wasm": "0.126.0",
|
|
52
52
|
"@types/node": "24.19.0",
|
|
53
53
|
"@vitest/browser-playwright": "5.0.2",
|
|
54
54
|
"fast-check": "4.10.2",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"pdfjs-dist": "6.4.299"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
|
-
"@quillmark/wasm": ">=0.
|
|
66
|
+
"@quillmark/wasm": ">=0.126.0"
|
|
67
67
|
},
|
|
68
68
|
"publishConfig": {
|
|
69
69
|
"access": "public"
|