blitz-pdf 0.1.7 → 0.1.9
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/README.md +44 -5
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/dist/api.d.ts +2 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +28 -3
- package/dist/api.js.map +1 -1
- package/dist/browser.d.ts +2 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +1 -1
- package/dist/browser.js.map +1 -1
- package/dist/node.d.ts +2 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +6 -2
- package/dist/node.js.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/worker-host.d.ts +12 -5
- package/dist/worker-host.d.ts.map +1 -1
- package/dist/worker-host.js +187 -20
- package/dist/worker-host.js.map +1 -1
- package/dist/worker-protocol.d.ts +5 -0
- package/dist/worker-protocol.d.ts.map +1 -1
- package/dist/worker-runtime.d.ts +1 -1
- package/dist/worker-runtime.d.ts.map +1 -1
- package/dist/worker-runtime.js +22 -6
- package/dist/worker-runtime.js.map +1 -1
- package/licenses/RUST_DEPENDENCIES.txt +34 -40
- package/package.json +1 -1
- package/src/api.ts +29 -4
- package/src/browser.ts +2 -1
- package/src/node.ts +7 -2
- package/src/types.ts +15 -0
- package/src/worker-host.ts +176 -23
- package/src/worker-protocol.ts +5 -5
- package/src/worker-runtime.ts +20 -6
- package/vendor/README.md +19 -5
- package/vendor/blitz-dom/Cargo.toml +6 -6
- package/vendor/blitz-dom/src/accessibility.rs +175 -2
- package/vendor/blitz-dom/src/config.rs +8 -10
- package/vendor/blitz-dom/src/document.rs +66 -3
- package/vendor/blitz-dom/src/events/keyboard.rs +2 -6
- package/vendor/blitz-dom/src/layout/construct.rs +5 -300
- package/vendor/blitz-dom/src/layout/inline/build.rs +309 -0
- package/vendor/blitz-dom/src/layout/{inline.rs → inline/layout.rs} +1 -1
- package/vendor/blitz-dom/src/layout/inline/mod.rs +4 -0
- package/vendor/blitz-dom/src/layout/mod.rs +3 -1
- package/vendor/blitz-dom/src/layout/replaced.rs +13 -4
- package/vendor/blitz-dom/src/mutator.rs +47 -0
- package/vendor/blitz-dom/src/node/element.rs +17 -0
- package/vendor/blitz-dom/src/node/mod.rs +1 -0
- package/vendor/blitz-dom/src/node/node.rs +46 -5
- package/vendor/blitz-dom/src/node/text.rs +11 -11
- package/vendor/blitz-dom/src/resolve.rs +2 -2
- package/vendor/blitz-dom/src/stylo.rs +30 -22
- package/vendor/blitz-dom/src/util.rs +1 -1
- package/vendor/blitz-paint/Cargo.toml +4 -4
- package/wasm/blitz_pdf_bg.wasm +2 -2
package/src/worker-protocol.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { RenderOptions } from "./render.js";
|
|
2
2
|
|
|
3
3
|
export type HostMessage =
|
|
4
|
-
| { type: "render"; module: WebAssembly.Module; html: string; options: RenderOptions; external: boolean }
|
|
5
|
-
| { type: "assets"; data: Uint8Array[] };
|
|
4
|
+
| { type: "render"; id: number; module: WebAssembly.Module; html: string; options: RenderOptions; external: boolean }
|
|
5
|
+
| { type: "assets"; id: number; data: Uint8Array[] };
|
|
6
6
|
|
|
7
7
|
export type WorkerMessage =
|
|
8
|
-
| { type: "assets"; urls: string[] }
|
|
9
|
-
| { type: "done"; pdf: Uint8Array }
|
|
10
|
-
| { type: "error"; error: unknown };
|
|
8
|
+
| { type: "assets"; id: number; urls: string[] }
|
|
9
|
+
| { type: "done"; id: number; pdf: Uint8Array }
|
|
10
|
+
| { type: "error"; id: number; error: unknown };
|
package/src/worker-runtime.ts
CHANGED
|
@@ -7,29 +7,43 @@ export interface WorkerEndpoint {
|
|
|
7
7
|
onMessage(handler: (message: HostMessage) => void): void;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
/**
|
|
10
|
+
/** One document at a time; retain the initialized engine and its bundled-font cache. */
|
|
11
11
|
export function serve(endpoint: WorkerEndpoint): void {
|
|
12
|
+
let initialized = false;
|
|
13
|
+
let activeId: number | undefined;
|
|
12
14
|
let receiveAssets: ((data: Uint8Array[]) => void) | undefined;
|
|
13
15
|
endpoint.onMessage(async message => {
|
|
14
16
|
if (message.type === "assets") {
|
|
17
|
+
if (message.id !== activeId) return;
|
|
15
18
|
const receive = receiveAssets;
|
|
16
19
|
receiveAssets = undefined;
|
|
17
20
|
receive?.(message.data);
|
|
18
21
|
return;
|
|
19
22
|
}
|
|
23
|
+
if (activeId !== undefined) {
|
|
24
|
+
endpoint.post({ type: "error", id: message.id, error: new Error("Worker already has an active document") }, []);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
activeId = message.id;
|
|
20
28
|
try {
|
|
21
|
-
|
|
29
|
+
if (!initialized) {
|
|
30
|
+
initSync({ module: message.module });
|
|
31
|
+
initialized = true;
|
|
32
|
+
}
|
|
22
33
|
const load = message.external
|
|
23
34
|
? (urls: string[]) => new Promise<Uint8Array[]>(resolve => {
|
|
24
35
|
receiveAssets = resolve;
|
|
25
|
-
endpoint.post({ type: "assets", urls }, []);
|
|
36
|
+
endpoint.post({ type: "assets", id: message.id, urls }, []);
|
|
26
37
|
})
|
|
27
38
|
: undefined;
|
|
28
39
|
const pdf = await render(message.html, message.options, load);
|
|
29
|
-
endpoint.post({ type: "done", pdf }, [pdf.buffer as ArrayBuffer]);
|
|
40
|
+
endpoint.post({ type: "done", id: message.id, pdf }, [pdf.buffer as ArrayBuffer]);
|
|
30
41
|
} catch (error) {
|
|
31
|
-
try { endpoint.post({ type: "error", error }, []); }
|
|
32
|
-
catch { endpoint.post({ type: "error", error: new Error(String(error)) }, []); }
|
|
42
|
+
try { endpoint.post({ type: "error", id: message.id, error }, []); }
|
|
43
|
+
catch { endpoint.post({ type: "error", id: message.id, error: new Error(String(error)) }, []); }
|
|
44
|
+
} finally {
|
|
45
|
+
activeId = undefined;
|
|
46
|
+
receiveAssets = undefined;
|
|
33
47
|
}
|
|
34
48
|
});
|
|
35
49
|
}
|
package/vendor/README.md
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
|
+
# Upstream versions
|
|
2
|
+
|
|
3
|
+
Checked against upstream on 2026-10-21:
|
|
4
|
+
|
|
5
|
+
- Blitz DOM and paint: `0.3.0-beta.4`, revision
|
|
6
|
+
`f8723f495c7c6bfb611a923a21ed5c8e0fe95d55` from `DioxusLabs/blitz` main.
|
|
7
|
+
All sibling Blitz crates use the same revision and its Taffy pin
|
|
8
|
+
`4e59968eda29ceeb0bbaeb3993c8cd38baa3d0a6`.
|
|
9
|
+
- Stylo `0.22.0`, Krilla `0.8.2`, and Fontique `0.12.0` are the latest
|
|
10
|
+
published crates.io releases. Their sources were compared with the release
|
|
11
|
+
archives to confirm that only the patches and packaging files below differ.
|
|
12
|
+
|
|
1
13
|
# Stylo paged-media patch
|
|
2
14
|
|
|
3
|
-
`stylo/`
|
|
15
|
+
`stylo/` contains the crates.io `stylo` 0.22.0 source (original crate checksum
|
|
16
|
+
`7a255e46af2478628c48daab3774090caabd7812b1dd3ca98801c2b9c53491ed`),
|
|
17
|
+
with the following changes:
|
|
4
18
|
|
|
5
19
|
- `properties/longhands.toml`: enable `break-before`, `break-after`, and
|
|
6
20
|
`break-inside` and the `@page size` descriptor for Servo; add inherited
|
|
@@ -44,7 +58,7 @@ Node/browser checks, and `npm run test:pdfa` against the rebuilt WASM.
|
|
|
44
58
|
# Blitz print layout patch
|
|
45
59
|
|
|
46
60
|
`blitz-dom/` contains `packages/blitz-dom` from Blitz revision
|
|
47
|
-
`
|
|
61
|
+
`f8723f495c7c6bfb611a923a21ed5c8e0fe95d55`, with its MIT/Apache-2.0 licenses.
|
|
48
62
|
Its standalone manifest expands the upstream workspace dependencies and pins
|
|
49
63
|
the sibling Blitz crates to the same revision.
|
|
50
64
|
|
|
@@ -54,11 +68,11 @@ the sibling Blitz crates to the same revision.
|
|
|
54
68
|
The PDF body and margin templates disable rounding and supply the exact
|
|
55
69
|
printable dimensions, including after CSS page setup changes them. Layout,
|
|
56
70
|
transforms, painting, and pagination share the resulting geometry.
|
|
57
|
-
- `layout/
|
|
71
|
+
- `layout/inline/build.rs`: use baseline alignment for the root of an inline
|
|
58
72
|
formatting context. A table cell's `vertical-align: middle` aligns the cell
|
|
59
73
|
content within its row; applying it again to its own text inflated every line.
|
|
60
74
|
Descendant inline alignment and the cell's placement remain intact.
|
|
61
|
-
- `layout/inline.rs`: let empty inline contexts compute their size and margin
|
|
75
|
+
- `layout/inline/layout.rs`: let empty inline contexts compute their size and margin
|
|
62
76
|
collapse normally. The empty-text shortcut prevented margins from collapsing
|
|
63
77
|
through paragraphs containing empty spans and discarded explicit heights.
|
|
64
78
|
- `layout/table.rs`: keep `max-width` as an intrinsic measurement constraint,
|
|
@@ -79,7 +93,7 @@ table columns, anonymous cells, and inheritance after CSS page-size changes.
|
|
|
79
93
|
# Blitz Unicode paint notifications
|
|
80
94
|
|
|
81
95
|
`blitz-paint/` contains `packages/blitz-paint` from Blitz revision
|
|
82
|
-
`
|
|
96
|
+
`f8723f495c7c6bfb611a923a21ed5c8e0fe95d55`, with its MIT/Apache-2.0 licenses.
|
|
83
97
|
Its standalone manifest expands the upstream workspace dependencies and pins
|
|
84
98
|
sibling crates to the same revision.
|
|
85
99
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name = "blitz-dom"
|
|
3
3
|
description = "Blitz DOM implementation"
|
|
4
4
|
documentation = "https://docs.rs/blitz-dom"
|
|
5
|
-
version = "0.3.0-beta.
|
|
5
|
+
version = "0.3.0-beta.4"
|
|
6
6
|
license = "MIT OR Apache-2.0"
|
|
7
7
|
homepage = "https://github.com/dioxuslabs/blitz"
|
|
8
8
|
repository = "https://github.com/dioxuslabs/blitz"
|
|
@@ -45,9 +45,9 @@ log-phase-times = ["debug_timer/enable"]
|
|
|
45
45
|
|
|
46
46
|
[dependencies]
|
|
47
47
|
# Blitz dependencies
|
|
48
|
-
blitz-traits = { version = "=0.3.0-beta.
|
|
49
|
-
stylo_taffy = { version = "=0.3.0-beta.
|
|
50
|
-
debug_timer = { version = "0.1.2", git = "https://github.com/DioxusLabs/blitz", rev = "
|
|
48
|
+
blitz-traits = { version = "=0.3.0-beta.4", default-features = false, git = "https://github.com/DioxusLabs/blitz", rev = "f8723f495c7c6bfb611a923a21ed5c8e0fe95d55" }
|
|
49
|
+
stylo_taffy = { version = "=0.3.0-beta.4", default-features = false, git = "https://github.com/DioxusLabs/blitz", rev = "f8723f495c7c6bfb611a923a21ed5c8e0fe95d55", features = ["default"] }
|
|
50
|
+
debug_timer = { version = "0.1.2", git = "https://github.com/DioxusLabs/blitz", rev = "f8723f495c7c6bfb611a923a21ed5c8e0fe95d55" }
|
|
51
51
|
|
|
52
52
|
# Servo dependencies
|
|
53
53
|
style = { version = "0.22.0", package = "stylo" }
|
|
@@ -64,7 +64,7 @@ smallvec = { version = "1" }
|
|
|
64
64
|
thin-vec = { version = "0.2" }
|
|
65
65
|
|
|
66
66
|
# DioxusLabs dependencies
|
|
67
|
-
taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "
|
|
67
|
+
taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "4e59968eda29ceeb0bbaeb3993c8cd38baa3d0a6", default-features = false, features = ["std", "flexbox", "flexbox_balance", "grid", "block_layout", "content_size", "calc", "detailed_layout_info"] }
|
|
68
68
|
anyrender = { version = "0.14.0", optional = true }
|
|
69
69
|
|
|
70
70
|
# Linebender/Fontations dependencies
|
|
@@ -100,7 +100,7 @@ percent-encoding = { version = "2.3.1" }
|
|
|
100
100
|
url = { version = "2.5.0" }
|
|
101
101
|
|
|
102
102
|
# Input
|
|
103
|
-
keyboard-types = { version = "0.
|
|
103
|
+
keyboard-types = { version = "0.8" }
|
|
104
104
|
cursor-icon = { version = "1" }
|
|
105
105
|
thread_local = { version = "1" }
|
|
106
106
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
use crate::{
|
|
1
|
+
use crate::{
|
|
2
|
+
BaseDocument, ElementData, Node as BlitzDomNode, NodeData, NodeId as BlitzDomNodeId, local_name,
|
|
3
|
+
};
|
|
2
4
|
use accesskit::{Node as AccessKitNode, NodeId, Role, TreeId, TreeInfo, TreeUpdate};
|
|
3
5
|
use style::properties::longhands::visibility;
|
|
4
6
|
|
|
@@ -7,6 +9,8 @@ impl BaseDocument {
|
|
|
7
9
|
let mut nodes = std::collections::HashMap::new();
|
|
8
10
|
let mut window = AccessKitNode::new(Role::Window);
|
|
9
11
|
let mut hidden_nodes = std::collections::HashSet::new();
|
|
12
|
+
let mut labelled_by_nodes = Vec::new();
|
|
13
|
+
let mut described_by_nodes = Vec::new();
|
|
10
14
|
|
|
11
15
|
self.visit(|node_id, node| {
|
|
12
16
|
if node.is_hidden_from_accessibility_tree()
|
|
@@ -25,9 +29,57 @@ impl BaseDocument {
|
|
|
25
29
|
.unwrap_or(&mut window);
|
|
26
30
|
let (id, builder) = self.build_accessibility_node(node, parent);
|
|
27
31
|
|
|
32
|
+
if node.attr(local_name!("aria-labelledby")).is_some() {
|
|
33
|
+
labelled_by_nodes.push(node_id);
|
|
34
|
+
}
|
|
35
|
+
if node.attr(local_name!("aria-describedby")).is_some() {
|
|
36
|
+
described_by_nodes.push(node_id);
|
|
37
|
+
}
|
|
28
38
|
nodes.insert(node_id, (id, builder));
|
|
29
39
|
});
|
|
30
40
|
|
|
41
|
+
let mut label = FlatText::default();
|
|
42
|
+
for node_id in labelled_by_nodes {
|
|
43
|
+
let Some(id_refs) = self.nodes[node_id].attr(local_name!("aria-labelledby")) else {
|
|
44
|
+
continue;
|
|
45
|
+
};
|
|
46
|
+
let Some((id, mut builder)) = nodes.remove(&node_id) else {
|
|
47
|
+
continue;
|
|
48
|
+
};
|
|
49
|
+
let mut has_target = false;
|
|
50
|
+
label.clear();
|
|
51
|
+
for target in self.referenced_dom_ids_to_node_ids(id_refs) {
|
|
52
|
+
if !has_target {
|
|
53
|
+
builder.clear_labelled_by();
|
|
54
|
+
has_target = true;
|
|
55
|
+
}
|
|
56
|
+
if target == node_id || nodes.contains_key(&target) {
|
|
57
|
+
builder.push_labelled_by(NodeId(target.as_u64()));
|
|
58
|
+
}
|
|
59
|
+
self.nodes[target].write_text_alternative(&mut label);
|
|
60
|
+
label.break_word();
|
|
61
|
+
}
|
|
62
|
+
if !label.text.is_empty() {
|
|
63
|
+
builder.set_label(label.text.as_str());
|
|
64
|
+
}
|
|
65
|
+
nodes.insert(node_id, (id, builder));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for node_id in described_by_nodes {
|
|
69
|
+
let Some(id_refs) = self.nodes[node_id].attr(local_name!("aria-describedby")) else {
|
|
70
|
+
continue;
|
|
71
|
+
};
|
|
72
|
+
let Some((id, mut builder)) = nodes.remove(&node_id) else {
|
|
73
|
+
continue;
|
|
74
|
+
};
|
|
75
|
+
for target in self.referenced_dom_ids_to_node_ids(id_refs) {
|
|
76
|
+
if target == node_id || nodes.contains_key(&target) {
|
|
77
|
+
builder.push_described_by(NodeId(target.as_u64()));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
nodes.insert(node_id, (id, builder));
|
|
81
|
+
}
|
|
82
|
+
|
|
31
83
|
let mut nodes: Vec<_> = nodes
|
|
32
84
|
.into_iter()
|
|
33
85
|
.map(|(_, (id, node))| (id, node))
|
|
@@ -43,6 +95,15 @@ impl BaseDocument {
|
|
|
43
95
|
}
|
|
44
96
|
}
|
|
45
97
|
|
|
98
|
+
fn referenced_dom_ids_to_node_ids<'a>(
|
|
99
|
+
&'a self,
|
|
100
|
+
nodes_reference: &'a str,
|
|
101
|
+
) -> impl Iterator<Item = BlitzDomNodeId> + 'a {
|
|
102
|
+
nodes_reference
|
|
103
|
+
.split_ascii_whitespace()
|
|
104
|
+
.filter_map(|dom_id| self.nodes_to_id.get(dom_id)?.first().copied())
|
|
105
|
+
}
|
|
106
|
+
|
|
46
107
|
fn build_accessibility_node(
|
|
47
108
|
&self,
|
|
48
109
|
node: &BlitzDomNode,
|
|
@@ -65,12 +126,48 @@ impl BaseDocument {
|
|
|
65
126
|
.unwrap_or(Role::Unknown);
|
|
66
127
|
|
|
67
128
|
builder.set_role(role);
|
|
68
|
-
builder.set_html_tag(name);
|
|
69
129
|
|
|
70
130
|
// https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion
|
|
71
131
|
if element_data.attr(local_name!("aria-hidden")) == Some("true") {
|
|
72
132
|
builder.set_hidden();
|
|
73
133
|
}
|
|
134
|
+
|
|
135
|
+
if let Some(aria_expanded) = element_data.attr(local_name!("aria-expanded"))
|
|
136
|
+
&& let Ok(aria_expanded) = aria_expanded.parse()
|
|
137
|
+
{
|
|
138
|
+
builder.set_expanded(aria_expanded);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if element_data.attr(local_name!("aria-disabled")) == Some("true") {
|
|
142
|
+
builder.set_disabled();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if let Some(aria_selected) = element_data.attr(local_name!("aria-selected"))
|
|
146
|
+
&& let Ok(aria_selected) = aria_selected.parse()
|
|
147
|
+
{
|
|
148
|
+
builder.set_selected(aria_selected);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if let Some(aria_label) = element_data.attr(local_name!("aria-label")) {
|
|
152
|
+
builder.set_label(aria_label);
|
|
153
|
+
}
|
|
154
|
+
if let Some(aria_description) = element_data.attr(local_name!("aria-description")) {
|
|
155
|
+
builder.set_description(aria_description);
|
|
156
|
+
}
|
|
157
|
+
if let Some(aria_level) = element_data.attr(local_name!("aria-level"))
|
|
158
|
+
&& let Ok(aria_level) = aria_level.parse::<usize>()
|
|
159
|
+
// Accesskit levels are 0-based, while ARIA levels are 1-based
|
|
160
|
+
// See https://docs.rs/accesskit/latest/accesskit/struct.Node.html#method.level
|
|
161
|
+
&& let Some(aria_level) = aria_level.checked_sub(1)
|
|
162
|
+
{
|
|
163
|
+
builder.set_level(aria_level);
|
|
164
|
+
} else if let Some(default_level) = default_level_for_tag(&name) {
|
|
165
|
+
builder.set_level(default_level);
|
|
166
|
+
} else if let Some(default_level) = default_level_for_role(role) {
|
|
167
|
+
builder.set_level(default_level);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
builder.set_html_tag(name);
|
|
74
171
|
} else if node.is_text_node() {
|
|
75
172
|
builder.set_role(Role::TextRun);
|
|
76
173
|
builder.set_value(node.text_content());
|
|
@@ -96,6 +193,59 @@ impl BlitzDomNode {
|
|
|
96
193
|
})
|
|
97
194
|
.unwrap_or(false)
|
|
98
195
|
}
|
|
196
|
+
|
|
197
|
+
fn write_text_alternative(&self, out: &mut FlatText) {
|
|
198
|
+
match self
|
|
199
|
+
.attr(local_name!("aria-label"))
|
|
200
|
+
.filter(|aria_label| !aria_label.trim_ascii().is_empty())
|
|
201
|
+
{
|
|
202
|
+
Some(aria_label) => out.push(aria_label),
|
|
203
|
+
None => self.write_flat_text_content(out),
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
fn write_flat_text_content(&self, out: &mut FlatText) {
|
|
208
|
+
match &self.data {
|
|
209
|
+
NodeData::Text(data) => out.push(&data.content),
|
|
210
|
+
NodeData::Element(..) | NodeData::AnonymousBlock(..) => {
|
|
211
|
+
for &child_id in &self.children {
|
|
212
|
+
self.with(child_id).write_flat_text_content(out);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
_ => {}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
#[derive(Default)]
|
|
221
|
+
struct FlatText {
|
|
222
|
+
text: String,
|
|
223
|
+
pending_space: bool,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
impl FlatText {
|
|
227
|
+
fn push(&mut self, text: &str) {
|
|
228
|
+
for (i, word) in text.split(|c: char| c.is_ascii_whitespace()).enumerate() {
|
|
229
|
+
self.pending_space |= i > 0;
|
|
230
|
+
if word.is_empty() {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if self.pending_space && !self.text.is_empty() {
|
|
234
|
+
self.text.push(' ');
|
|
235
|
+
}
|
|
236
|
+
self.pending_space = false;
|
|
237
|
+
self.text.push_str(word);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
fn break_word(&mut self) {
|
|
242
|
+
self.pending_space = true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
fn clear(&mut self) {
|
|
246
|
+
self.text.clear();
|
|
247
|
+
self.pending_space = false;
|
|
248
|
+
}
|
|
99
249
|
}
|
|
100
250
|
|
|
101
251
|
fn role_from_name(name: &str) -> Option<Role> {
|
|
@@ -162,6 +312,29 @@ fn role_from_name(name: &str) -> Option<Role> {
|
|
|
162
312
|
}
|
|
163
313
|
}
|
|
164
314
|
|
|
315
|
+
fn default_level_for_tag(name: &str) -> Option<usize> {
|
|
316
|
+
// Accesskit levels are 0-based, while ARIA levels are 1-based
|
|
317
|
+
// See https://docs.rs/accesskit/latest/accesskit/struct.Node.html#method.level
|
|
318
|
+
match name {
|
|
319
|
+
"h1" => Some(0),
|
|
320
|
+
"h2" => Some(1),
|
|
321
|
+
"h3" => Some(2),
|
|
322
|
+
"h4" => Some(3),
|
|
323
|
+
"h5" => Some(4),
|
|
324
|
+
"h6" => Some(5),
|
|
325
|
+
_ => None,
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
fn default_level_for_role(role: Role) -> Option<usize> {
|
|
330
|
+
match role {
|
|
331
|
+
// ARIA specifies an implicit level of 2 for the heading role. Accesskit levels are 0-based,
|
|
332
|
+
// so this turns into level 1
|
|
333
|
+
Role::Heading => Some(1),
|
|
334
|
+
_ => None,
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
165
338
|
fn role_from_element_data(element_data: &ElementData) -> Option<Role> {
|
|
166
339
|
// <https://www.w3.org/TR/html-aam-1.0/>
|
|
167
340
|
match &*element_data.name.local {
|
|
@@ -10,20 +10,18 @@ use style::media_queries::MediaType;
|
|
|
10
10
|
|
|
11
11
|
/// Strategy for Stylo's style traversal during `resolve`.
|
|
12
12
|
///
|
|
13
|
-
///
|
|
14
|
-
///
|
|
15
|
-
///
|
|
16
|
-
///
|
|
17
|
-
///
|
|
18
|
-
/// user thread while another `Parallel` resolve is in flight.
|
|
13
|
+
/// `Parallel` traversals are serialised process-wide (Stylo's global pool can
|
|
14
|
+
/// only run one traversal at a time), so documents resolving from several
|
|
15
|
+
/// threads never run their style traversals concurrently but also never
|
|
16
|
+
/// overlap: it only pays off when a single thread resolves all documents
|
|
17
|
+
/// (e.g. a UI thread, including nested sub-documents).
|
|
19
18
|
#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)]
|
|
20
19
|
pub enum StyleThreading {
|
|
21
20
|
/// Use Stylo's parallel traversal via its global rayon thread pool.
|
|
22
|
-
/// Fastest for a single document; panics if another `Parallel` resolve
|
|
23
|
-
/// is in flight on a different thread.
|
|
24
21
|
Parallel,
|
|
25
22
|
/// Run style traversal sequentially on the calling thread, bypassing
|
|
26
|
-
/// the global pool
|
|
23
|
+
/// the global pool and its lock. Threads resolving their own documents
|
|
24
|
+
/// run independently of each other.
|
|
27
25
|
#[default]
|
|
28
26
|
Sequential,
|
|
29
27
|
}
|
|
@@ -57,7 +55,7 @@ pub struct DocumentConfig {
|
|
|
57
55
|
/// Defaults to [`MediaType::screen`].
|
|
58
56
|
pub media_type: Option<MediaType>,
|
|
59
57
|
/// Strategy for Stylo's style traversal.
|
|
60
|
-
/// Defaults to [`StyleThreading::
|
|
58
|
+
/// Defaults to [`StyleThreading::Sequential`].
|
|
61
59
|
pub style_threading: StyleThreading,
|
|
62
60
|
/// Whether incremental layout is enabled. Defaults to `true`.
|
|
63
61
|
///
|
|
@@ -954,6 +954,21 @@ impl BaseDocument {
|
|
|
954
954
|
if self.focus_node_id == Some(node_id) {
|
|
955
955
|
let shell_provider = self.shell_provider.clone();
|
|
956
956
|
self.nodes[node_id].blur(shell_provider);
|
|
957
|
+
// Nothing on the chain contains the focus anymore. Only the surviving
|
|
958
|
+
// ancestors need a snapshot for restyling: nodes in the removed subtree had
|
|
959
|
+
// theirs dropped already and may be freed entirely.
|
|
960
|
+
let mut maybe_id = Some(node_id);
|
|
961
|
+
while let Some(node) = maybe_id.and_then(|id| self.get_node(id)) {
|
|
962
|
+
let id = node.id;
|
|
963
|
+
maybe_id = node.parent;
|
|
964
|
+
if node.flags.is_in_document() {
|
|
965
|
+
self.snapshot_node_and(id, ElementState::FOCUS_WITHIN, |node| {
|
|
966
|
+
node.remove_focus_within()
|
|
967
|
+
});
|
|
968
|
+
} else {
|
|
969
|
+
self.nodes[id].remove_focus_within();
|
|
970
|
+
}
|
|
971
|
+
}
|
|
957
972
|
self.focus_node_id = None;
|
|
958
973
|
}
|
|
959
974
|
if self.mousedown_node_id == Some(node_id) {
|
|
@@ -1699,7 +1714,13 @@ impl BaseDocument {
|
|
|
1699
1714
|
self.snapshot_node_and(id, ElementState::FOCUS | ElementState::FOCUSRING, |node| {
|
|
1700
1715
|
node.blur(shell_provider)
|
|
1701
1716
|
});
|
|
1717
|
+
for &ancestor in self.node_element_ancestors(id).iter() {
|
|
1718
|
+
self.snapshot_node_and(ancestor, ElementState::FOCUS_WITHIN, |node| {
|
|
1719
|
+
node.remove_focus_within()
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1702
1722
|
self.focus_node_id = None;
|
|
1723
|
+
self.shell_provider.request_redraw();
|
|
1703
1724
|
}
|
|
1704
1725
|
}
|
|
1705
1726
|
|
|
@@ -1713,6 +1734,10 @@ impl BaseDocument {
|
|
|
1713
1734
|
if Some(focus_node_id) == self.focus_node_id {
|
|
1714
1735
|
return false;
|
|
1715
1736
|
}
|
|
1737
|
+
// A node that is not in the document cannot be focused
|
|
1738
|
+
if !self.nodes[focus_node_id].flags.is_in_document() {
|
|
1739
|
+
return false;
|
|
1740
|
+
}
|
|
1716
1741
|
|
|
1717
1742
|
#[cfg(feature = "tracing")]
|
|
1718
1743
|
tracing::info!("Focussed node {focus_node_id}");
|
|
@@ -1733,8 +1758,32 @@ impl BaseDocument {
|
|
|
1733
1758
|
|node| node.focus(shell_provider),
|
|
1734
1759
|
);
|
|
1735
1760
|
|
|
1761
|
+
// Update :focus-within on the ancestor chains (shared prefix stays)
|
|
1762
|
+
let old_node_path = self.maybe_node_element_ancestors(self.focus_node_id);
|
|
1763
|
+
let new_node_path = self.node_element_ancestors(focus_node_id);
|
|
1764
|
+
let same_count = old_node_path
|
|
1765
|
+
.iter()
|
|
1766
|
+
.zip(&new_node_path)
|
|
1767
|
+
.take_while(|(o, n)| o == n)
|
|
1768
|
+
.count();
|
|
1769
|
+
for &id in old_node_path.iter().skip(same_count) {
|
|
1770
|
+
self.snapshot_node_and(id, ElementState::FOCUS_WITHIN, |node| {
|
|
1771
|
+
node.remove_focus_within()
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
for &id in new_node_path.iter().skip(same_count) {
|
|
1775
|
+
self.snapshot_node_and(id, ElementState::FOCUS_WITHIN, |node| {
|
|
1776
|
+
node.add_focus_within()
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1736
1780
|
self.focus_node_id = Some(focus_node_id);
|
|
1737
1781
|
|
|
1782
|
+
// The focus is styled (:focus, :focus-visible, :focus-within), so
|
|
1783
|
+
// moving it changes what is on screen. Nothing else asks for the
|
|
1784
|
+
// frame: a keyboard focus change touches no layout and no content.
|
|
1785
|
+
self.shell_provider.request_redraw();
|
|
1786
|
+
|
|
1738
1787
|
true
|
|
1739
1788
|
}
|
|
1740
1789
|
|
|
@@ -2211,11 +2260,15 @@ impl BaseDocument {
|
|
|
2211
2260
|
// Prefer the precise hit node: `cursor` and `user-select` may be set on
|
|
2212
2261
|
// a pseudo-element or resolved on an anonymous box, and text hits carry
|
|
2213
2262
|
// is_text via the hit node. Fall back to the canonical hover node if
|
|
2214
|
-
// the hit node has been removed (it is transient across resolves)
|
|
2215
|
-
|
|
2263
|
+
// the hit node has been removed (it is transient across resolves), and
|
|
2264
|
+
// to the default cursor if the pointer is over no box.
|
|
2265
|
+
let Some(node_id) = self
|
|
2216
2266
|
.hover_hit_node_id
|
|
2217
2267
|
.filter(|&id| self.nodes.contains_key(id))
|
|
2218
|
-
.or(self.get_hover_node_id())
|
|
2268
|
+
.or(self.get_hover_node_id())
|
|
2269
|
+
else {
|
|
2270
|
+
return Some(CursorIcon::Default);
|
|
2271
|
+
};
|
|
2219
2272
|
let node = &self.nodes[node_id];
|
|
2220
2273
|
|
|
2221
2274
|
if let Some(subdoc) = node.subdoc().map(|doc| doc.inner())
|
|
@@ -2953,6 +3006,16 @@ mod hover_state_tests {
|
|
|
2953
3006
|
assert_eq!(doc.get_hover_node_id(), Some(container));
|
|
2954
3007
|
assert_eq!(doc.get_cursor(), Some(CursorIcon::Default));
|
|
2955
3008
|
}
|
|
3009
|
+
|
|
3010
|
+
/// Hovering below the content hits no box: default cursor (`None` would hide it).
|
|
3011
|
+
#[test]
|
|
3012
|
+
fn hovering_below_content_reports_default_cursor() {
|
|
3013
|
+
let (mut doc, _) = make_doc();
|
|
3014
|
+
|
|
3015
|
+
doc.set_hover_to(5.0, 250.0);
|
|
3016
|
+
assert_eq!(doc.get_hover_node_id(), None);
|
|
3017
|
+
assert_eq!(doc.get_cursor(), Some(CursorIcon::Default));
|
|
3018
|
+
}
|
|
2956
3019
|
}
|
|
2957
3020
|
|
|
2958
3021
|
#[cfg(test)]
|
|
@@ -5,7 +5,7 @@ use blitz_traits::{
|
|
|
5
5
|
SmolStr,
|
|
6
6
|
events::{BlitzInputEvent, BlitzKeyEvent, DomEvent, DomEventData},
|
|
7
7
|
};
|
|
8
|
-
use keyboard_types::{Key, Modifiers};
|
|
8
|
+
use keyboard_types::{Key, Modifiers, NamedKey};
|
|
9
9
|
use markup5ever::local_name;
|
|
10
10
|
|
|
11
11
|
pub(super) enum KeyboardOrTextInputEvent {
|
|
@@ -20,9 +20,8 @@ pub(crate) fn handle_key_or_input_event<F: FnMut(DomEvent)>(
|
|
|
20
20
|
mut dispatch_event: F,
|
|
21
21
|
) {
|
|
22
22
|
if let KeyboardOrTextInputEvent::KeyPress(event) = &event {
|
|
23
|
-
if event.key == Key::Tab {
|
|
23
|
+
if event.key == Key::Named(NamedKey::Tab) {
|
|
24
24
|
let backwards = event.modifiers.contains(Modifiers::SHIFT);
|
|
25
|
-
let old_focus = doc.focus_node_id;
|
|
26
25
|
generate_focus_events(
|
|
27
26
|
doc,
|
|
28
27
|
&mut |doc| {
|
|
@@ -34,9 +33,6 @@ pub(crate) fn handle_key_or_input_event<F: FnMut(DomEvent)>(
|
|
|
34
33
|
},
|
|
35
34
|
&mut dispatch_event,
|
|
36
35
|
);
|
|
37
|
-
if doc.focus_node_id != old_focus {
|
|
38
|
-
doc.shell_provider.request_redraw();
|
|
39
|
-
}
|
|
40
36
|
return;
|
|
41
37
|
}
|
|
42
38
|
|