@quario/html 0.1.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/CHANGELOG.md +17 -0
- package/LICENSE +219 -0
- package/README.md +186 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.js +333 -0
- package/lib/style.css +102 -0
- package/package.json +65 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to @quario/html are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-08-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **HTML fragments for compiled reports.** Semantic tables, stable `q-*`
|
|
15
|
+
classes, every interpolated value escaped. Pass `html()` to `report.render`
|
|
16
|
+
and wrap the fragment in your own page. The default look ships as
|
|
17
|
+
`@quario/html/style.css`, not as inline style in the markup.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# Quario License Agreement
|
|
2
|
+
|
|
3
|
+
> **In short:** Quario is commercial software with visible source.
|
|
4
|
+
> Evaluate it free for as long as you like. Evaluation output is
|
|
5
|
+
> watermarked. Anything else needs a per-developer license, after
|
|
6
|
+
> which deployment is unlimited and royalty-free. This summary is not
|
|
7
|
+
> the license; the full terms below govern.
|
|
8
|
+
|
|
9
|
+
Version 1.0, August 2026
|
|
10
|
+
|
|
11
|
+
This agreement is between you and Webstronauts, trading as Quario,
|
|
12
|
+
established in the Netherlands ("we", "us"), and governs the Software
|
|
13
|
+
defined below.
|
|
14
|
+
|
|
15
|
+
Quario is commercial software. Its packages ship with readable source
|
|
16
|
+
code, but it is not open source. No rights are granted beyond those in
|
|
17
|
+
this agreement.
|
|
18
|
+
|
|
19
|
+
## 1. Definitions
|
|
20
|
+
|
|
21
|
+
- The **Software** is any software we publish under the Quario name,
|
|
22
|
+
including the `quario` and `@quario/*` packages, except where a
|
|
23
|
+
product states its own terms. Each release states the edition of this
|
|
24
|
+
agreement that governs it.
|
|
25
|
+
- **You** means the individual exercising rights under this agreement
|
|
26
|
+
and, where that individual acts for a company or other organization,
|
|
27
|
+
that organization. In that case the organization is the licensee, and
|
|
28
|
+
the rights and limits in this agreement apply to it as a whole.
|
|
29
|
+
- An **Application** is software you create that incorporates the
|
|
30
|
+
Software and adds substantial functionality beyond it. A
|
|
31
|
+
licensee-operated service that embeds the Software and renders
|
|
32
|
+
reports for your End Users is an Application under this agreement,
|
|
33
|
+
even if it does little more than render.
|
|
34
|
+
- A **Developer** is any individual (employee or contractor working on
|
|
35
|
+
your behalf) who writes or maintains code that uses the Software's
|
|
36
|
+
APIs, or who creates or maintains report definitions for use with the
|
|
37
|
+
Software. That includes people who never install or run the Software
|
|
38
|
+
themselves, when their definitions are rendered by a service that
|
|
39
|
+
embeds it.
|
|
40
|
+
- A **Seat** is a paid license for one named Developer, purchased from
|
|
41
|
+
us or an authorized reseller.
|
|
42
|
+
- **End Users** are people who interact with the Software only through
|
|
43
|
+
an Application: running it, viewing the reports it produces, or
|
|
44
|
+
supplying parameters or other input to existing report definitions
|
|
45
|
+
the Application provides. Creating or maintaining report definitions
|
|
46
|
+
makes someone a Developer. End Users do not purchase a license; their
|
|
47
|
+
rights come from section 5.
|
|
48
|
+
|
|
49
|
+
## 2. Acceptance
|
|
50
|
+
|
|
51
|
+
This file is our standing offer. Anyone who exercises the evaluation
|
|
52
|
+
rights in section 3 accepts them, subject to their conditions. All
|
|
53
|
+
other rights arise when Seats are purchased. Where a purchase is made
|
|
54
|
+
for an organization, the person completing it confirms that they are
|
|
55
|
+
authorized to accept this agreement on the organization's behalf, and
|
|
56
|
+
acceptance covers the whole organization. If you do not accept this
|
|
57
|
+
agreement, do not use the Software.
|
|
58
|
+
|
|
59
|
+
## 3. Evaluation
|
|
60
|
+
|
|
61
|
+
You may download, install, and use the Software free of charge to
|
|
62
|
+
evaluate it: assessing whether it suits your purposes, and building
|
|
63
|
+
prototypes or proofs of concept. Evaluation covers the full feature
|
|
64
|
+
set, needs no key, and has no fixed time limit. The Software marks
|
|
65
|
+
evaluation output as described in section 6.
|
|
66
|
+
|
|
67
|
+
Evaluation is limited to assessment: you may not distribute any
|
|
68
|
+
Application containing the Software, use the Software in live business
|
|
69
|
+
operations, or expose it in services offered to third parties. The
|
|
70
|
+
moment your use goes beyond evaluating the Software, it requires Seats
|
|
71
|
+
under section 4.
|
|
72
|
+
|
|
73
|
+
## 4. Developer licenses
|
|
74
|
+
|
|
75
|
+
Any use of the Software beyond section 3 requires a Seat for every
|
|
76
|
+
Developer, whether or not an Application has shipped. With those Seats
|
|
77
|
+
in place:
|
|
78
|
+
|
|
79
|
+
- Each licensed Developer may install and use the Software on any number
|
|
80
|
+
of machines for developing Applications.
|
|
81
|
+
- Automated build, test, and CI systems do not consume Seats.
|
|
82
|
+
- Seats are per named individual. They may be reassigned when a
|
|
83
|
+
Developer stops working with the Software, but not shared or pooled
|
|
84
|
+
between concurrently active Developers. Contractors' work for you is
|
|
85
|
+
covered by your Seats; work they do for other clients is not.
|
|
86
|
+
|
|
87
|
+
## 5. Distribution and runtime
|
|
88
|
+
|
|
89
|
+
With Seats under section 4 in place, you may deploy and operate your
|
|
90
|
+
Applications yourself, and distribute the Software as an embedded part
|
|
91
|
+
of them, **royalty-free**: no additional fees or per-deployment
|
|
92
|
+
licenses are required for any number of servers, copies, or End Users.
|
|
93
|
+
You may embed your license key in an Application and ship it at no
|
|
94
|
+
charge, so section 6 does not mark that Application's output.
|
|
95
|
+
|
|
96
|
+
For that purpose, we grant you the right to pass through to each End
|
|
97
|
+
User, and to anyone who distributes or operates your Application on your
|
|
98
|
+
behalf, a limited, non-exclusive, non-transferable right to execute the
|
|
99
|
+
Software only as embedded in the Application. You must not permit them
|
|
100
|
+
to extract the Software from the Application or use it to develop
|
|
101
|
+
software of their own.
|
|
102
|
+
|
|
103
|
+
If you operate a service that embeds the Software and renders reports
|
|
104
|
+
for your End Users, that service is an Application under section 1. You
|
|
105
|
+
may run it royalty-free under this section, and you may put your license
|
|
106
|
+
key in it as in any Application. Calling applications that only request
|
|
107
|
+
renders from it need not incorporate the Software. People who use those
|
|
108
|
+
applications only as End Users under section 1 get the pass-through
|
|
109
|
+
rights above.
|
|
110
|
+
|
|
111
|
+
You may not:
|
|
112
|
+
|
|
113
|
+
- distribute the Software on its own, or as part of a product whose
|
|
114
|
+
primary purpose is to provide report-engine, report-design, or
|
|
115
|
+
document-generation capability to third-party developers;
|
|
116
|
+
- expose the Software's APIs to third parties as a development tool or
|
|
117
|
+
service that substitutes for those parties buying their own Seats. A
|
|
118
|
+
licensee-operated service that embeds the Software and renders for
|
|
119
|
+
your own End Users falls outside that ban;
|
|
120
|
+
- fork, mirror, repackage, rename, or wrap the Software, or republish
|
|
121
|
+
it on any package registry or code-hosting platform, other than as
|
|
122
|
+
embedded in an Application.
|
|
123
|
+
|
|
124
|
+
For as long as this agreement is in force, you may not create any tool,
|
|
125
|
+
library, product, or service that competes with the Software or any
|
|
126
|
+
other Quario product, whether the competition is direct or indirect;
|
|
127
|
+
this restriction does not survive termination. You may not remove or
|
|
128
|
+
alter copyright or license notices in the Software. If your Application
|
|
129
|
+
is itself a closed development product for third-party developers (OEM
|
|
130
|
+
redistribution), contact us for separate terms.
|
|
131
|
+
|
|
132
|
+
## 6. License keys and output marking
|
|
133
|
+
|
|
134
|
+
The Software accepts a license key that evidences your Seats under
|
|
135
|
+
section 4. Key checks run offline.
|
|
136
|
+
|
|
137
|
+
Without a valid key, or with a key whose validity period does not cover
|
|
138
|
+
the installed version's release date, the Software runs with its full
|
|
139
|
+
feature set but marks its output as unlicensed. Using a key you are not
|
|
140
|
+
entitled to is a material breach of this agreement.
|
|
141
|
+
|
|
142
|
+
You may not remove, disable, or circumvent the key check or the output
|
|
143
|
+
marking, or distribute anything that does.
|
|
144
|
+
|
|
145
|
+
## 7. Term, updates, and perpetual fallback
|
|
146
|
+
|
|
147
|
+
Seats are sold as an annual subscription. While the subscription is
|
|
148
|
+
active, licensed Developers receive all Software versions we release,
|
|
149
|
+
and support for the Software through our issue tracker and by email. We
|
|
150
|
+
provide support with commercially reasonable effort; we do not guarantee
|
|
151
|
+
specific response times.
|
|
152
|
+
|
|
153
|
+
If the subscription lapses, your license to the versions released during
|
|
154
|
+
your subscription is **perpetual, for the number of Seats held at the
|
|
155
|
+
lapse**: up to that many Developers may keep using those versions, and
|
|
156
|
+
your Applications may keep shipping them, forever, at no further cost.
|
|
157
|
+
Versions released after the lapse require renewal. We encode this rule
|
|
158
|
+
in purchased keys: a key remains valid for every version released
|
|
159
|
+
during its validity period, with no end date.
|
|
160
|
+
|
|
161
|
+
## 8. Source availability
|
|
162
|
+
|
|
163
|
+
The Software's packages ship with readable source code so that you can
|
|
164
|
+
review and debug what you run. Readable source does not grant extra
|
|
165
|
+
rights: this agreement governs all use of the source, and you may
|
|
166
|
+
publish the Software (modified or not) only as part of an Application
|
|
167
|
+
under section 5.
|
|
168
|
+
|
|
169
|
+
## 9. Ownership, trademarks, and feedback
|
|
170
|
+
|
|
171
|
+
We license the Software; we retain all intellectual property rights in
|
|
172
|
+
it. Report definitions, data, and output you create with the Software
|
|
173
|
+
are yours. "Quario" and the Quario logo are our trademarks; this
|
|
174
|
+
agreement grants no rights to use them beyond identifying the Software.
|
|
175
|
+
If you send us feedback or suggestions, we may use them without
|
|
176
|
+
obligation.
|
|
177
|
+
|
|
178
|
+
## 10. Warranty and liability
|
|
179
|
+
|
|
180
|
+
We provide the Software as is. To the fullest extent the law allows, we
|
|
181
|
+
disclaim all warranties and conditions, express or implied, including
|
|
182
|
+
merchantability, fitness for a particular purpose, and non-infringement.
|
|
183
|
+
|
|
184
|
+
To the fullest extent the law allows: we are not liable for indirect or
|
|
185
|
+
consequential damages, loss of profits, loss of data, or business
|
|
186
|
+
interruption; and our total aggregate liability under this agreement is
|
|
187
|
+
limited to the greater of EUR 100 and the fees you paid us in the
|
|
188
|
+
twelve months preceding the event giving rise to the claim. These
|
|
189
|
+
limitations do not apply where the damage results from the intent or
|
|
190
|
+
deliberate recklessness (opzet of bewuste roekeloosheid) of us or our
|
|
191
|
+
management, or where mandatory law does not permit them.
|
|
192
|
+
|
|
193
|
+
## 11. Termination and survival
|
|
194
|
+
|
|
195
|
+
If you materially breach this agreement, we may terminate it by notice.
|
|
196
|
+
If the breach can be remedied, we will first allow 30 days from that
|
|
197
|
+
notice to remedy it, and termination takes effect only if the breach
|
|
198
|
+
remains. On termination, the licenses granted here end.
|
|
199
|
+
Perpetual-fallback rights under section 7 survive termination only if
|
|
200
|
+
they arose from a lapse that predates the breach, and section 5
|
|
201
|
+
survives with them to the extent needed to keep exercising them.
|
|
202
|
+
Sections 8 through 12 survive any termination.
|
|
203
|
+
|
|
204
|
+
## 12. General
|
|
205
|
+
|
|
206
|
+
Dutch law governs this agreement. Disputes go to the competent court for
|
|
207
|
+
our registered office. You may not assign this agreement without our
|
|
208
|
+
consent, except to a successor of your whole business; we may assign it
|
|
209
|
+
to a successor of ours. If a provision is unenforceable, the remainder
|
|
210
|
+
stays in force. Failure to enforce a provision does not waive it. This
|
|
211
|
+
agreement, together with your order for Seats, is the entire agreement
|
|
212
|
+
about the Software, unless we agree otherwise in writing.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
**Purchasing and contact:** <contact@getquario.com> ·
|
|
217
|
+
<https://getquario.com>
|
|
218
|
+
|
|
219
|
+
Copyright © 2026 Webstronauts. All rights reserved.
|
package/README.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# @quario/html
|
|
2
|
+
|
|
3
|
+
**The HTML render target for [quario](https://www.npmjs.com/package/quario).** Renders a report
|
|
4
|
+
definition to an HTML fragment with semantic tables, a stable class contract, and every
|
|
5
|
+
interpolated value escaped.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install quario @quario/html
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The engine is a peer dependency, installed beside the target. ESM-only, Node 22+, and
|
|
14
|
+
browser-ready through any standards-based ESM bundler.
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
import { quario } from "quario";
|
|
20
|
+
import { html } from "@quario/html";
|
|
21
|
+
|
|
22
|
+
const schema = {
|
|
23
|
+
data: "$.orders[*]",
|
|
24
|
+
aggregates: { total: "sum:=@.price * @.qty" },
|
|
25
|
+
groups: [
|
|
26
|
+
{
|
|
27
|
+
name: "region",
|
|
28
|
+
by: "=@.region",
|
|
29
|
+
aggregates: { subtotal: "sum:=@.price * @.qty" },
|
|
30
|
+
header: [{ type: "text", value: "{{ region.key }}", style: { bold: true } }],
|
|
31
|
+
footer: [{ type: "text", value: "Subtotal {{ money(region.subtotal) }}" }],
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
detail: {
|
|
35
|
+
columns: [
|
|
36
|
+
{ header: "Product", value: "{{ @.product }}" },
|
|
37
|
+
{ header: "Amount", value: "{{ money(@.price * @.qty) }}", style: { align: "right" } },
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
footer: [{ type: "text", value: "Grand total {{ money($.total) }}" }],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const data = { orders: [{ region: "North", product: "Desk", price: 250, qty: 2 }] };
|
|
44
|
+
|
|
45
|
+
const report = quario().report(schema, { money: (n) => "$" + n.toFixed(2) });
|
|
46
|
+
const page = await report.render(html(), data);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```html
|
|
50
|
+
<div class="q-group" data-group="region">
|
|
51
|
+
<div class="q-item q-group-header" style="font-weight:bold">North</div>
|
|
52
|
+
<table class="q-table">
|
|
53
|
+
<colgroup><col><col></colgroup>
|
|
54
|
+
<thead><tr><th>Product</th><th>Amount</th></tr></thead>
|
|
55
|
+
<tbody><tr><td>Desk</td><td style="text-align:right">$500.00</td></tr></tbody>
|
|
56
|
+
</table>
|
|
57
|
+
<div class="q-item q-group-footer">Subtotal $500.00</div>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="q-item q-report-footer">Grand total $500.00</div>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The output is a **fragment**: no `<html>`, no `<head>`, no styles of its own. Wrap it in your own
|
|
63
|
+
page shell and stylesheet.
|
|
64
|
+
|
|
65
|
+
## API
|
|
66
|
+
|
|
67
|
+
### `html()`
|
|
68
|
+
|
|
69
|
+
The target factory takes no options and returns the target you pass to `render`.
|
|
70
|
+
`report()` compiles once and `report.render(html(), data)` resolves the fragment. Compile at
|
|
71
|
+
startup and render per request. Definition problems throw at `report()`, at compile time.
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
const report = quario().report(schema, funcs);
|
|
75
|
+
const page = await report.render(html(), data);
|
|
76
|
+
|
|
77
|
+
report.names; // free variable names the expressions read
|
|
78
|
+
report.functions; // registry function names the definition calls
|
|
79
|
+
report.paths; // the data query's frozen dependency topology
|
|
80
|
+
report.stream(data); // the raw event generator, if you want events instead
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Engine-level options (`query` budgets, the license key) live on the instance
|
|
84
|
+
(`quario({ query, license })`); see the [engine README](https://www.npmjs.com/package/quario).
|
|
85
|
+
|
|
86
|
+
Rendering is asynchronous and hands the loop back between batches, so a large report never blocks the
|
|
87
|
+
host. Compilation stays synchronous; render-time failures reject with the same located errors.
|
|
88
|
+
|
|
89
|
+
## Output contract
|
|
90
|
+
|
|
91
|
+
These classes are the contract host CSS targets. They are stable, and changing them is a breaking
|
|
92
|
+
change.
|
|
93
|
+
|
|
94
|
+
| Emits | For |
|
|
95
|
+
| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
96
|
+
| `<div class="q-item q-<role>">` | Every item. Roles: `q-report-header`, `q-empty`, `q-group-header`, `q-detail`, `q-group-footer`, `q-report-footer` |
|
|
97
|
+
| `<div class="q-group" data-group="name">` | Each group instance, wrapping its header, nested content, and footer. Adds `q-break` when the group declares `break: "page"` or `reset: "page"` |
|
|
98
|
+
| `<table class="q-table">` | Each table, with a real `colgroup`, `thead`, `tbody`, and a `tfoot` when totals are declared |
|
|
99
|
+
| `<div class="q-item q-image q-<role>">` | Each image item, holding one `<img>` whose `src` is a base64 `data:` URI of the event's bytes; the rendered `alt` is escaped |
|
|
100
|
+
|
|
101
|
+
A column `width` becomes an inline `width:<n>%` on its `<col>`. The classes above belong to this
|
|
102
|
+
target. The schema never speaks in CSS. Displaying a fragment that contains images under a Content
|
|
103
|
+
Security Policy needs `img-src data:`.
|
|
104
|
+
|
|
105
|
+
Style declarations map to inline CSS (`bold` → `font-weight:bold`, `size` → `font-size:<n>pt`,
|
|
106
|
+
`family: "mono"` → `font-family:monospace`, …). Only those mappings appear in the markup. This
|
|
107
|
+
target supplies no defaults in the markup: no weight or size per band role, no leading, no padding,
|
|
108
|
+
no spacing between bands, no borders. An unstyled report renders as unstyled markup, and the
|
|
109
|
+
classes above are where a stylesheet supplies the look. An inline `style` attribute would beat
|
|
110
|
+
yours in the cascade and force `!important` on you; the defaults ship as an ordinary stylesheet
|
|
111
|
+
instead, at [`@quario/html/style.css`](#the-reference-stylesheet). The PDF and XLSX targets carry
|
|
112
|
+
theirs built in, because their consumers have no stylesheet. An empty row set still emits the
|
|
113
|
+
table, its header, and an empty `<tbody>`. A hidden table cell keeps its `<td>`, empty.
|
|
114
|
+
|
|
115
|
+
This target is unpaginated: it ignores schema page bands (page furniture belongs to your print CSS)
|
|
116
|
+
and maps `break: "page"` and `reset: "page"` to the `q-break` class. Page columns are accepted on
|
|
117
|
+
the schema and ignored here until a target honors them.
|
|
118
|
+
|
|
119
|
+
## Unlicensed marking
|
|
120
|
+
|
|
121
|
+
An unlicensed render opens the fragment with `<div class="q-unlicensed">` holding the wording
|
|
122
|
+
from `report-start.marking` as escaped text. A licensed render emits no badge. The element and its
|
|
123
|
+
position are normative; visual treatment belongs to the host. The shipped stylesheet leaves the
|
|
124
|
+
badge unstyled, because a watermark would escape the fragment's box onto the host page.
|
|
125
|
+
|
|
126
|
+
## Escaping
|
|
127
|
+
|
|
128
|
+
**Every interpolated value is escaped.** `{{{ }}}` is a definition error, so no schema syntax can
|
|
129
|
+
exempt a data value from escaping. Generated attribute values, including classes and inline
|
|
130
|
+
styles built from expressions, are escaped too.
|
|
131
|
+
|
|
132
|
+
Literal template text passes through verbatim as author-controlled markup. A definition is
|
|
133
|
+
trusted configuration. Its _data_ is untrusted, and data can never reach the document unescaped.
|
|
134
|
+
|
|
135
|
+
## Printing to PDF
|
|
136
|
+
|
|
137
|
+
For a browserless, deterministic document, use
|
|
138
|
+
[`@quario/pdf`](https://www.npmjs.com/package/@quario/pdf), same schema, same API, a
|
|
139
|
+
`Uint8Array` out. Use the HTML print route below when you want full CSS typography and already
|
|
140
|
+
have a browser or Paged-CSS engine in your pipeline.
|
|
141
|
+
|
|
142
|
+
Host CSS targets these classes:
|
|
143
|
+
|
|
144
|
+
1. Render the fragment and wrap it in a document with two stylesheets inlined, in order:
|
|
145
|
+
`@quario/html/style.css` below, then your own.
|
|
146
|
+
2. The shipped sheet already puts band behavior on the emitted classes:
|
|
147
|
+
`.q-group { break-inside: avoid }` keeps a group header with its rows,
|
|
148
|
+
`.q-break { break-before: page }` honors the schema's own break hint, and `thead` repeats per
|
|
149
|
+
printed page. Yours adds the page geometry in `@page`, which only you can decide.
|
|
150
|
+
3. Print with a headless browser (`page.pdf()` in Playwright or Puppeteer, page numbers via the
|
|
151
|
+
footer template) or a Paged-CSS engine (WeasyPrint, Prince; page numbers via `@page` margin
|
|
152
|
+
boxes).
|
|
153
|
+
|
|
154
|
+
### The reference stylesheet
|
|
155
|
+
|
|
156
|
+
The package ships the default look as a real stylesheet, `@quario/html/style.css`: the band-role
|
|
157
|
+
weights and sizes, the table look, and the four pagination rules above. Link it, import it through
|
|
158
|
+
a bundler, or read and inline it. Plain Node cannot `import` a `.css` file, so on the server:
|
|
159
|
+
|
|
160
|
+
```js
|
|
161
|
+
import { readFileSync } from "node:fs";
|
|
162
|
+
|
|
163
|
+
const css = readFileSync(new URL(import.meta.resolve("@quario/html/style.css")), "utf8");
|
|
164
|
+
const page = `<!doctype html><html><head><style>${css}</style></head><body>${fragment}</body></html>`;
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Its **selectors** are contract; its **rules** are not. It is a reference default meant to be
|
|
168
|
+
overridden. Every rule is ordinary specificity, so your own stylesheet loaded after it wins
|
|
169
|
+
without `!important` anywhere. Link nothing at all and you get unstyled markup.
|
|
170
|
+
|
|
171
|
+
Two things it leaves to you: `@page` geometry and body type, which only a host can decide, and
|
|
172
|
+
any watermark treatment of `.q-unlicensed`, left as plain text here because `position: fixed`
|
|
173
|
+
would paint over your whole page. `example/print.css` in the repository is a host's half showing
|
|
174
|
+
both, and `example/print.js` is the complete pipeline.
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
Commercial software with readable source. Free, unlimited, watermarked evaluation; per-developer
|
|
179
|
+
licenses at [getquario.com](https://getquario.com). See the bundled LICENSE.
|
|
180
|
+
|
|
181
|
+
Pass your license key once, on the instance; it is verified offline:
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
const q = quario({ license: "quario_..." });
|
|
185
|
+
await q.license; // { licensed: true, licensee: "Acme BV", id: "1-ACME" }
|
|
186
|
+
```
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @quario/html — the HTML render target: a pure consumer of the engine's
|
|
3
|
+
* public event stream, passed to a compiled report's `render` and serializing
|
|
4
|
+
* each event to the HTML contract described in SCHEMA.md.
|
|
5
|
+
*
|
|
6
|
+
* All HTML escaping lives here, at the markup edge: every interpolated token
|
|
7
|
+
* value is escaped, literal template text passes verbatim as author-controlled
|
|
8
|
+
* markup, and every generated attribute value is escaped. The core stream
|
|
9
|
+
* never carries markup-escaped text.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { isReportBand, text, walk } from "quario";
|
|
13
|
+
|
|
14
|
+
/** @type {Record<string, string>} */
|
|
15
|
+
let ESC = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
16
|
+
/** @type {(value: any) => string} */
|
|
17
|
+
let esc = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => ESC[char]);
|
|
18
|
+
|
|
19
|
+
// Join a cell's tokens to HTML: literal template text is author-controlled
|
|
20
|
+
// markup and passes verbatim; every interpolated value is escaped, no
|
|
21
|
+
// exceptions. The engine's own `text` join is the other one -- display text,
|
|
22
|
+
// where a literal is words rather than markup -- and an attribute value takes
|
|
23
|
+
// that one, escaped whole.
|
|
24
|
+
/** @type {(tokens: any[]) => string} */
|
|
25
|
+
let markup = (tokens) => {
|
|
26
|
+
let out = "";
|
|
27
|
+
for (let token of tokens) out += "literal" in token ? token.literal : esc(token.value);
|
|
28
|
+
return out;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// base64 for an image's `data:` URI, without Buffer: this package runs in Node
|
|
32
|
+
// and in ES2024 browsers off the same `lib/`, and `btoa` is a standard global
|
|
33
|
+
// in both. The walk is chunked because `fromCharCode(...bytes)` spreads one
|
|
34
|
+
// argument per byte, which a real image exhausts.
|
|
35
|
+
let CHUNK = 0x8000;
|
|
36
|
+
/** @type {(bytes: Uint8Array) => string} */
|
|
37
|
+
let base64 = (bytes) => {
|
|
38
|
+
let binary = "";
|
|
39
|
+
for (let at = 0; at < bytes.length; at += CHUNK)
|
|
40
|
+
binary += String.fromCharCode(...bytes.subarray(at, at + CHUNK));
|
|
41
|
+
return btoa(binary);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Map the neutral style vocabulary to inline CSS — the one place in the stack
|
|
45
|
+
// that speaks CSS. Every mapper coerces: a computed result of the wrong shape
|
|
46
|
+
// emits nothing, so untrusted data reaching a style expression can never
|
|
47
|
+
// smuggle CSS of its own into the attribute. Unknown names are ignored.
|
|
48
|
+
/** @type {Record<string, string>} */
|
|
49
|
+
let FAMILY = { sans: "sans-serif", serif: "serif", mono: "monospace" };
|
|
50
|
+
/** @type {(value: any) => boolean} */
|
|
51
|
+
let finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
52
|
+
let HEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
53
|
+
/** @type {(value: any) => boolean} */
|
|
54
|
+
let isHex = (value) => typeof value === "string" && HEX.test(value);
|
|
55
|
+
let ALIGNMENTS = ["left", "center", "right"];
|
|
56
|
+
|
|
57
|
+
// A group instance's own bands. They sit outside a columned container only when
|
|
58
|
+
// that instance is the node which declared the count — an inner instance's are
|
|
59
|
+
// content like anything else. The report's own bands are the engine's to name.
|
|
60
|
+
let OWN = new Set(["group-header", "group-footer"]);
|
|
61
|
+
/** @type {(value: string) => string} */
|
|
62
|
+
let named = (value) => {
|
|
63
|
+
let key = value.toLowerCase();
|
|
64
|
+
// Own-key lookup: `constructor` must not resolve an inherited member.
|
|
65
|
+
if (Object.hasOwn(FAMILY, key)) return FAMILY[key];
|
|
66
|
+
let name = value.replace(/[^A-Za-z0-9 _-]/g, "");
|
|
67
|
+
return name ? "'" + name + "'" : "";
|
|
68
|
+
};
|
|
69
|
+
/** @type {Record<string, (value: any) => string>} */
|
|
70
|
+
let CSS = {
|
|
71
|
+
family: (value) => {
|
|
72
|
+
if (typeof value !== "string" || !value) return "";
|
|
73
|
+
let name = named(value);
|
|
74
|
+
return name ? "font-family:" + name : "";
|
|
75
|
+
},
|
|
76
|
+
size: (value) => (finite(value) && value > 0 ? "font-size:" + value + "pt" : ""),
|
|
77
|
+
bold: (value) => (value ? "font-weight:bold" : ""),
|
|
78
|
+
italic: (value) => (value ? "font-style:italic" : ""),
|
|
79
|
+
color: (value) => (isHex(value) ? "color:" + value : ""),
|
|
80
|
+
background: (value) => (isHex(value) ? "background-color:" + value : ""),
|
|
81
|
+
align: (value) => (ALIGNMENTS.includes(value) ? "text-align:" + value : ""),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** @type {(name: string, value: any) => string} */
|
|
85
|
+
let decl = (name, value) => {
|
|
86
|
+
if (value == null || !Object.hasOwn(CSS, name)) return "";
|
|
87
|
+
return CSS[name](value);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** @type {(style: any) => string} */
|
|
91
|
+
let decoration = (style) => {
|
|
92
|
+
let parts = [];
|
|
93
|
+
if (style.underline) parts.push("underline");
|
|
94
|
+
if (style.strikethrough) parts.push("line-through");
|
|
95
|
+
return parts.length ? "text-decoration:" + parts.join(" ") : "";
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** @type {(name: string) => boolean} */
|
|
99
|
+
let isDecoration = (name) => name === "underline" || name === "strikethrough";
|
|
100
|
+
|
|
101
|
+
/** @type {(out: string[], style: any, seen: { decoration: boolean }) => void} */
|
|
102
|
+
let pushDecoration = (out, style, seen) => {
|
|
103
|
+
if (seen.decoration) return;
|
|
104
|
+
seen.decoration = true;
|
|
105
|
+
let deco = decoration(style);
|
|
106
|
+
if (deco) out.push(deco);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** @type {(out: string[], name: string, value: any) => void} */
|
|
110
|
+
let pushDecl = (out, name, value) => {
|
|
111
|
+
let declaration = decl(name, value);
|
|
112
|
+
if (declaration) out.push(declaration);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Serialize a resolved style object to inline CSS, in declaration order.
|
|
116
|
+
// underline and strikethrough share one CSS property, so the first of either
|
|
117
|
+
// in authored order emits the composed value and the second is skipped.
|
|
118
|
+
/** @type {(style: any) => string} */
|
|
119
|
+
let css = (style) => {
|
|
120
|
+
let out = /** @type {string[]} */ ([]);
|
|
121
|
+
let seen = { decoration: false };
|
|
122
|
+
for (let name in style) {
|
|
123
|
+
if (isDecoration(name)) pushDecoration(out, style, seen);
|
|
124
|
+
else pushDecl(out, name, style[name]);
|
|
125
|
+
}
|
|
126
|
+
return out.join(";");
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Compose a resolved style into one escaped inline style attribute.
|
|
130
|
+
/** @type {(event: { style?: any }) => string} */
|
|
131
|
+
let styleAttr = ({ style }) => {
|
|
132
|
+
let out = style ? css(style) : "";
|
|
133
|
+
return out ? ' style="' + esc(out) + '"' : "";
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/** @type {(list: any[], tag: string) => string} */
|
|
137
|
+
let cells = (list, tag) =>
|
|
138
|
+
list
|
|
139
|
+
.map((cell) => "<" + tag + styleAttr(cell) + ">" + markup(cell.tokens) + "</" + tag + ">")
|
|
140
|
+
.join("");
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The HTML render target:
|
|
144
|
+
* `quario().report(schema).render(html(), data)`.
|
|
145
|
+
*
|
|
146
|
+
* Rendering is async: the engine's `walk` driver dispatches events to the
|
|
147
|
+
* handlers below and hands the loop back between batches, so a very large
|
|
148
|
+
* report never blocks the event loop. This target emits an unpaginated
|
|
149
|
+
* fragment, so `report-start.page` band closures are ignored — page furniture
|
|
150
|
+
* belongs to the host's print CSS.
|
|
151
|
+
*
|
|
152
|
+
* @returns {{name: "html", compile: (stream: any) => (data?: any) => Promise<string>}}
|
|
153
|
+
* The target (see SCHEMA.md, "Instances and targets").
|
|
154
|
+
*/
|
|
155
|
+
export function html() {
|
|
156
|
+
return {
|
|
157
|
+
name: "html",
|
|
158
|
+
compile: (stream) => async (data) => {
|
|
159
|
+
let out = "";
|
|
160
|
+
// The finished `src` of every image this render has already encoded,
|
|
161
|
+
// keyed by the array the source expression yielded. A logo down a
|
|
162
|
+
// thousand rows is one base64 walk rather than a thousand, and the map
|
|
163
|
+
// goes with the render — nothing here outlives it.
|
|
164
|
+
/** @type {Map<Uint8Array, string>} */
|
|
165
|
+
let sources = new Map();
|
|
166
|
+
// Whether a `total-row` already closed the `<tbody>` — markup
|
|
167
|
+
// bookkeeping that stays at this edge, out of the event contract.
|
|
168
|
+
let tfoot = false;
|
|
169
|
+
// The page columns a node declared: its `count`, the `owner` depth that
|
|
170
|
+
// declared it (-1 for the report root), and whether the container is
|
|
171
|
+
// emitted and still to be closed. Null when no node owes one. Never more
|
|
172
|
+
// than one, because nesting a columned region is a definition error.
|
|
173
|
+
/** @type {{ count: number, owner: number, open: boolean } | null} */
|
|
174
|
+
let region = null;
|
|
175
|
+
// How many group instances are open, so the declaring node's own bands
|
|
176
|
+
// can be told from the content they bracket: a `group-header` or
|
|
177
|
+
// `group-footer` belongs to the innermost instance, which is the node
|
|
178
|
+
// that may own the region. Taken from each event rather than counted, so
|
|
179
|
+
// there is one spelling of a depth the stream already carries.
|
|
180
|
+
let depth = 0;
|
|
181
|
+
// Does this event stay full-width, outside the columns? The declaring
|
|
182
|
+
// node's own bands do.
|
|
183
|
+
/** @type {(event: any) => boolean} */
|
|
184
|
+
let fullWidth = (event) =>
|
|
185
|
+
isReportBand(event.role) ||
|
|
186
|
+
(OWN.has(event.role) && region != null && region.owner === depth - 1);
|
|
187
|
+
// Close the container if one is open. The count itself survives: a report
|
|
188
|
+
// header is full-width and arrives *before* the body it columns, so
|
|
189
|
+
// forgetting the count here would leave that body uncolumned.
|
|
190
|
+
let close = () => {
|
|
191
|
+
if (!region || !region.open) return;
|
|
192
|
+
region.open = false;
|
|
193
|
+
out += "</div>";
|
|
194
|
+
};
|
|
195
|
+
// Open the container the node owes, at the first content that belongs
|
|
196
|
+
// inside it — which is what leaves that node's own bands above it.
|
|
197
|
+
let openColumns = () => {
|
|
198
|
+
if (!region || region.open) return;
|
|
199
|
+
region.open = true;
|
|
200
|
+
out += '<div class="q-columns" style="column-count:' + esc(region.count) + '">';
|
|
201
|
+
};
|
|
202
|
+
// Everything that is not one of the declaring node's own bands is the
|
|
203
|
+
// content between them: it opens the container owed, or falls inside the
|
|
204
|
+
// one already open.
|
|
205
|
+
/** @type {(event: any) => void} */
|
|
206
|
+
let content = (event) => {
|
|
207
|
+
// The overwhelmingly common case, and the hottest path in the render:
|
|
208
|
+
// no node declared a count, so there is nothing to open or close.
|
|
209
|
+
if (!region) return;
|
|
210
|
+
if (fullWidth(event)) close();
|
|
211
|
+
else openColumns();
|
|
212
|
+
};
|
|
213
|
+
// Hidden items never reach the stream and a hidden cell arrives with no
|
|
214
|
+
// tokens, so no visibility checks are needed in any handler below.
|
|
215
|
+
await walk(stream(data), {
|
|
216
|
+
// Nothing unless the render is unlicensed — then the fragment opens
|
|
217
|
+
// with the marking badge (LICENSE section 6): the engine's wording in
|
|
218
|
+
// this target's element, escaped like every other value crossing this
|
|
219
|
+
// edge, so the badge stays childless whatever stream a host compiles
|
|
220
|
+
// against.
|
|
221
|
+
"report-start": (event) => {
|
|
222
|
+
if (event.marking) out += '<div class="q-unlicensed">' + esc(event.marking) + "</div>";
|
|
223
|
+
if (event.columns) region = { count: event.columns, owner: -1, open: false };
|
|
224
|
+
},
|
|
225
|
+
// The stable `q-item q-<role>` classes are the whole class attribute;
|
|
226
|
+
// print CSS targets the band-role class for styling and breaks.
|
|
227
|
+
item: (event) => {
|
|
228
|
+
content(event);
|
|
229
|
+
out +=
|
|
230
|
+
'<div class="q-item q-' +
|
|
231
|
+
event.role +
|
|
232
|
+
'"' +
|
|
233
|
+
styleAttr(event) +
|
|
234
|
+
">" +
|
|
235
|
+
markup(event.tokens) +
|
|
236
|
+
"</div>";
|
|
237
|
+
},
|
|
238
|
+
// An image is an item container of its own holding one `<img>`: the
|
|
239
|
+
// band-role class joins `q-item q-image`, `align`/`background` dress
|
|
240
|
+
// the container, and `fit` sizes the picture inside it. The `src` is
|
|
241
|
+
// this target's own base64 of the event's bytes under the MIME type
|
|
242
|
+
// the engine already sniffed -- generated, but emitted through the
|
|
243
|
+
// same escaped attribute path as everything else rather than a second
|
|
244
|
+
// one, and escaped when it is encoded rather than per occurrence. `alt` is report data, so it is the display-text join escaped,
|
|
245
|
+
// never the markup-passing join a cell body gets; an image without one
|
|
246
|
+
// carries `alt=""`, the decorative-image convention.
|
|
247
|
+
image: (event) => {
|
|
248
|
+
content(event);
|
|
249
|
+
let source = sources.get(event.bytes);
|
|
250
|
+
if (source === undefined)
|
|
251
|
+
sources.set(
|
|
252
|
+
event.bytes,
|
|
253
|
+
(source = esc("data:image/" + event.format + ";base64," + base64(event.bytes))),
|
|
254
|
+
);
|
|
255
|
+
out +=
|
|
256
|
+
'<div class="q-item q-image q-' +
|
|
257
|
+
event.role +
|
|
258
|
+
'"' +
|
|
259
|
+
styleAttr(event) +
|
|
260
|
+
'><img src="' +
|
|
261
|
+
source +
|
|
262
|
+
'" alt="' +
|
|
263
|
+
esc(text(event.alt || [])) +
|
|
264
|
+
// Both keep the aspect ratio, since neither names a height.
|
|
265
|
+
(event.fit === "width" ? '" style="width:100%">' : '" style="max-width:100%">') +
|
|
266
|
+
"</div>";
|
|
267
|
+
},
|
|
268
|
+
// Each group instance is wrapped in a `q-group` container carrying
|
|
269
|
+
// the group name, so print CSS can keep a header with its rows
|
|
270
|
+
// (`break-inside`) or force a break per group (`break-before`). A
|
|
271
|
+
// declared `break: 'page'` or `reset: 'page'` adds the `q-break` class
|
|
272
|
+
// for print CSS to map (`.q-break { break-before: page }`).
|
|
273
|
+
"group-start": (event) => {
|
|
274
|
+
content(event);
|
|
275
|
+
depth = event.depth + 1;
|
|
276
|
+
out +=
|
|
277
|
+
'<div class="q-group' +
|
|
278
|
+
(event.break || event.reset ? " q-break" : "") +
|
|
279
|
+
'"' +
|
|
280
|
+
' data-group="' +
|
|
281
|
+
esc(event.name) +
|
|
282
|
+
'">';
|
|
283
|
+
// Declared after the container the instance itself sits in opened,
|
|
284
|
+
// so a count on this group columns what it brackets rather than
|
|
285
|
+
// what brackets it.
|
|
286
|
+
if (event.columns) region = { count: event.columns, owner: event.depth, open: false };
|
|
287
|
+
},
|
|
288
|
+
"group-end": (event) => {
|
|
289
|
+
// The instance that owes or holds the columns is the one ending here:
|
|
290
|
+
// it closes the container and forgets the count, so nothing later
|
|
291
|
+
// can reopen one on its behalf.
|
|
292
|
+
if (region && region.owner === event.depth) {
|
|
293
|
+
close();
|
|
294
|
+
region = null;
|
|
295
|
+
}
|
|
296
|
+
depth = event.depth;
|
|
297
|
+
out += "</div>";
|
|
298
|
+
},
|
|
299
|
+
"table-start": (event) => {
|
|
300
|
+
content(event);
|
|
301
|
+
tfoot = false;
|
|
302
|
+
let cols = "";
|
|
303
|
+
for (let column of /** @type {any[]} */ (event.columns))
|
|
304
|
+
cols +=
|
|
305
|
+
"<col" + (column.width ? ' style="width:' + esc(column.width) + '%"' : "") + ">";
|
|
306
|
+
let headers = event.columns.map((/** @type {any} */ column) => column.header);
|
|
307
|
+
out +=
|
|
308
|
+
'<table class="q-table">' +
|
|
309
|
+
"<colgroup>" +
|
|
310
|
+
cols +
|
|
311
|
+
"</colgroup>" +
|
|
312
|
+
"<thead><tr>" +
|
|
313
|
+
cells(headers, "th") +
|
|
314
|
+
"</tr></thead><tbody>";
|
|
315
|
+
},
|
|
316
|
+
row: (event) => {
|
|
317
|
+
out += "<tr" + styleAttr(event) + ">" + cells(event.cells, "td") + "</tr>";
|
|
318
|
+
},
|
|
319
|
+
"total-row": (event) => {
|
|
320
|
+
tfoot = true;
|
|
321
|
+
out += "</tbody><tfoot><tr>" + cells(event.cells, "td") + "</tr></tfoot>";
|
|
322
|
+
},
|
|
323
|
+
"table-end": () => {
|
|
324
|
+
out += tfoot ? "</table>" : "</tbody></table>";
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
// A root region reaches here open when no full-width footer followed the
|
|
328
|
+
// body it columns.
|
|
329
|
+
close();
|
|
330
|
+
return out;
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|
package/lib/style.css
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The reference stylesheet for a quario HTML fragment: the default look this
|
|
3
|
+
* target deliberately does not put in the markup.
|
|
4
|
+
*
|
|
5
|
+
* `@quario/html` supplies no omakase defaults of its own -- an inline
|
|
6
|
+
* `style` attribute would beat a host's stylesheet in the cascade and force
|
|
7
|
+
* `!important` on anyone restyling a default they never asked for. That rule
|
|
8
|
+
* is about where the bytes land, not about leaving the seam empty, so the
|
|
9
|
+
* defaults ship here instead: ordinary rules on the stable `q-*` classes,
|
|
10
|
+
* which any host rule of equal specificity overrides by source order alone.
|
|
11
|
+
* See docs/adr/0016-the-default-look-ships-as-a-stylesheet.md and
|
|
12
|
+
* docs/adr/0014-a-target-supplies-defaults-only-where-its-consumer-has-no-seam.md.
|
|
13
|
+
*
|
|
14
|
+
* The selector contract is normative (SCHEMA.md, "The HTML target"); these
|
|
15
|
+
* rules are not. They are a reference default -- adjust freely, or never link
|
|
16
|
+
* this file at all.
|
|
17
|
+
*
|
|
18
|
+
* The block between the `shared:` markers below is byte-identical to the one
|
|
19
|
+
* in `packages/viewer/lib/style.js`, which the viewer adopts into its shadow
|
|
20
|
+
* root. The two cannot be single-sourced -- the viewer imports no target
|
|
21
|
+
* package -- so `test/stylesheet.test.js` at the repo root fails when they
|
|
22
|
+
* drift. Edit one, edit the other.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/* shared:start */
|
|
26
|
+
.q-table {
|
|
27
|
+
width: 100%;
|
|
28
|
+
border-collapse: collapse;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.q-table th,
|
|
32
|
+
.q-table td {
|
|
33
|
+
padding: 2pt 6pt;
|
|
34
|
+
text-align: left;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.q-table thead th {
|
|
38
|
+
border-bottom: 0.5pt solid #000;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.q-table tfoot td {
|
|
42
|
+
border-top: 0.5pt solid #000;
|
|
43
|
+
font-weight: bold;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.q-item.q-report-header {
|
|
47
|
+
font-size: 14pt;
|
|
48
|
+
font-weight: bold;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.q-item.q-group-header {
|
|
52
|
+
margin-top: 8pt;
|
|
53
|
+
font-weight: bold;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.q-item.q-group-footer {
|
|
57
|
+
margin-bottom: 8pt;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.q-item.q-report-footer {
|
|
61
|
+
margin-top: 10pt;
|
|
62
|
+
border-top: 1pt solid #000;
|
|
63
|
+
padding-top: 4pt;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* A columned container is a fragmentation context on screen, not only in
|
|
67
|
+
print, so this is what keeps a label whole in one page column -- the rule
|
|
68
|
+
SCHEMA.md's "Page columns" leans on. Scoped to a columned region, so an
|
|
69
|
+
uncolumned report pays nothing. Inside the shared block because both sheets
|
|
70
|
+
need it: a sheet has no pages, but it can have columns. */
|
|
71
|
+
.q-columns .q-group {
|
|
72
|
+
break-inside: avoid;
|
|
73
|
+
}
|
|
74
|
+
/* shared:end */
|
|
75
|
+
|
|
76
|
+
/*
|
|
77
|
+
* Fragmentation. SCHEMA.md calls these the print-CSS contract, so they ship
|
|
78
|
+
* with the look rather than being left to the host to rediscover: `thead`
|
|
79
|
+
* repeats per printed page, a table row stays whole, a group header travels
|
|
80
|
+
* with its rows, and a group declaring `break: "page"` gets one.
|
|
81
|
+
*
|
|
82
|
+
* Pages are not the only fragmentation context: the columns case lives in the
|
|
83
|
+
* shared block above, because a sheet with no pages can still have columns.
|
|
84
|
+
*
|
|
85
|
+
* A host overrides any of these the ordinary way; `example/print.css` turns
|
|
86
|
+
* `break-inside` back to `auto` for one group to let it span pages.
|
|
87
|
+
*/
|
|
88
|
+
.q-table thead {
|
|
89
|
+
display: table-header-group;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.q-table tr {
|
|
93
|
+
break-inside: avoid;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.q-group {
|
|
97
|
+
break-inside: avoid;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.q-break {
|
|
101
|
+
break-before: page;
|
|
102
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@quario/html",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The HTML render target for quario — in the makings, not yet released",
|
|
5
|
+
"homepage": "https://getquario.com",
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/getquario/quario.git",
|
|
10
|
+
"directory": "packages/html"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"CHANGELOG.md",
|
|
14
|
+
"lib"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"types": "lib/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./lib/index.d.ts",
|
|
21
|
+
"default": "./lib/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./style.css": "./lib/style.css",
|
|
24
|
+
"./package.json": "./package.json"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"check": "npm run size && npm test && npm run test:browser",
|
|
31
|
+
"size": "size-limit",
|
|
32
|
+
"test": "npm run test:unit && npm run test:types",
|
|
33
|
+
"test:browser": "node test/browser/setup.js",
|
|
34
|
+
"test:types": "tsc && attw --pack . --profile esm-only --exclude-entrypoints style.css",
|
|
35
|
+
"test:unit": "node --disallow-code-generation-from-strings --test --test-concurrency=1 test/*.test.js",
|
|
36
|
+
"prepack": "node -e \"require('fs').copyFileSync('../../LICENSE','LICENSE')\"",
|
|
37
|
+
"postpack": "node -e \"require('fs').rmSync('LICENSE',{force:true})\""
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@arethetypeswrong/cli": "^0.18.3",
|
|
41
|
+
"@size-limit/preset-small-lib": "^13.0.3",
|
|
42
|
+
"quario": "^0.1.0",
|
|
43
|
+
"size-limit": "^13.0.3",
|
|
44
|
+
"typescript": "^7.0.2"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"quario": "^0.1.0"
|
|
48
|
+
},
|
|
49
|
+
"size-limit": [
|
|
50
|
+
{
|
|
51
|
+
"path": "lib/index.js",
|
|
52
|
+
"limit": "1.7 kB",
|
|
53
|
+
"ignore": [
|
|
54
|
+
"quario"
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"path": "lib/style.css",
|
|
59
|
+
"limit": "1 kB"
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": ">=22.0.0"
|
|
64
|
+
}
|
|
65
|
+
}
|