@effindomv2/fui-rs 0.1.3
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/COMMERCIAL.md +7 -0
- package/Cargo.toml +34 -0
- package/LICENSE.md +9 -0
- package/README.md +88 -0
- package/package.json +54 -0
- package/scripts/build.sh +227 -0
- package/scripts/check-runtime-dependency.sh +41 -0
- package/scripts/framework-host-services.ts +40 -0
- package/scripts/generate-host-events.ts +17 -0
- package/scripts/generate-host-services.ts +28 -0
- package/scripts/hostgen/common.ts +73 -0
- package/scripts/hostgen/registry.ts +159 -0
- package/scripts/hostgen/rust-host-events.ts +171 -0
- package/scripts/hostgen/rust-host-services.ts +257 -0
- package/src/animation.rs +625 -0
- package/src/app.rs +324 -0
- package/src/assets.rs +572 -0
- package/src/bindings/mod.rs +1 -0
- package/src/bindings/ui.rs +705 -0
- package/src/bitmap.rs +317 -0
- package/src/bridge_callbacks.rs +242 -0
- package/src/context_menu_manager.rs +332 -0
- package/src/controls/anti_selection_area.rs +54 -0
- package/src/controls/button.rs +542 -0
- package/src/controls/checkbox.rs +372 -0
- package/src/controls/combobox.rs +1574 -0
- package/src/controls/context_menu.rs +1367 -0
- package/src/controls/control_template_set.rs +46 -0
- package/src/controls/control_tokens.rs +596 -0
- package/src/controls/dialog.rs +590 -0
- package/src/controls/dropdown.rs +1010 -0
- package/src/controls/form.rs +229 -0
- package/src/controls/internal/button_presenter.rs +142 -0
- package/src/controls/internal/checkbox_indicator_presenter.rs +210 -0
- package/src/controls/internal/dropdown_chevron_presenter.rs +134 -0
- package/src/controls/internal/dropdown_field_presenter.rs +269 -0
- package/src/controls/internal/dropdown_option_row_presenter.rs +184 -0
- package/src/controls/internal/mod.rs +13 -0
- package/src/controls/internal/pressable_indicator_presenter.rs +26 -0
- package/src/controls/internal/pressable_labeled_control.rs +492 -0
- package/src/controls/internal/radio_indicator_presenter.rs +208 -0
- package/src/controls/internal/selectable_popup_list.rs +597 -0
- package/src/controls/internal/slider_presenter.rs +282 -0
- package/src/controls/internal/switch_indicator_presenter.rs +235 -0
- package/src/controls/internal/text_input_core.rs +1074 -0
- package/src/controls/internal/text_input_presenter.rs +108 -0
- package/src/controls/mod.rs +235 -0
- package/src/controls/nav_link.rs +395 -0
- package/src/controls/popup.rs +191 -0
- package/src/controls/progress_bar.rs +287 -0
- package/src/controls/radio_button.rs +348 -0
- package/src/controls/radio_group.rs +283 -0
- package/src/controls/selection_area.rs +82 -0
- package/src/controls/shared.rs +68 -0
- package/src/controls/slider.rs +701 -0
- package/src/controls/switch.rs +293 -0
- package/src/controls/templating.rs +49 -0
- package/src/controls/tests.rs +3905 -0
- package/src/controls/text_area.rs +207 -0
- package/src/controls/text_input.rs +192 -0
- package/src/debug.rs +146 -0
- package/src/drag_drop.rs +424 -0
- package/src/drag_gesture.rs +258 -0
- package/src/drawing.rs +385 -0
- package/src/event.rs +1603 -0
- package/src/external_drop.rs +467 -0
- package/src/fetch.rs +500 -0
- package/src/ffi.rs +3542 -0
- package/src/file.rs +1677 -0
- package/src/focus_adorner.rs +307 -0
- package/src/focus_visibility.rs +121 -0
- package/src/frame_scheduler.rs +159 -0
- package/src/frame_signal.rs +64 -0
- package/src/generated/ffi.rs +798 -0
- package/src/generated/framework_host_services.rs +74 -0
- package/src/generated/mod.rs +2 -0
- package/src/host_services.rs +162 -0
- package/src/image_sampling.rs +78 -0
- package/src/keyboard_scroll.rs +137 -0
- package/src/keyboard_scroll_tracker.rs +385 -0
- package/src/lib.rs +547 -0
- package/src/logger.rs +56 -0
- package/src/mobile_text_selection_toolbar.rs +1034 -0
- package/src/navigation.rs +48 -0
- package/src/node/core.rs +2210 -0
- package/src/node/custom_drawable.rs +149 -0
- package/src/node/flex_box.rs +874 -0
- package/src/node/grid.rs +304 -0
- package/src/node/helpers.rs +442 -0
- package/src/node/image.rs +506 -0
- package/src/node/mod.rs +315 -0
- package/src/node/scroll_bar.rs +845 -0
- package/src/node/scroll_box.rs +514 -0
- package/src/node/scroll_state.rs +121 -0
- package/src/node/scroll_view.rs +557 -0
- package/src/node/svg_node.rs +474 -0
- package/src/node/text_node.rs +633 -0
- package/src/node/virtual_list.rs +678 -0
- package/src/panic_hook.rs +36 -0
- package/src/persisted.rs +274 -0
- package/src/platform.rs +556 -0
- package/src/popup_presenter.rs +344 -0
- package/src/selection_handle_adorner.rs +838 -0
- package/src/signal.rs +134 -0
- package/src/text.rs +1065 -0
- package/src/theme.rs +571 -0
- package/src/timers.rs +106 -0
- package/src/tool_tip.rs +167 -0
- package/src/tool_tip_manager.rs +691 -0
- package/src/transitions.rs +41 -0
- package/src/typography.rs +520 -0
- package/src/viewport.rs +108 -0
- package/src/worker.rs +308 -0
- package/src/worker_job.rs +82 -0
- package/src/worker_runtime.rs +172 -0
package/COMMERCIAL.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# FUI-RS Commercial License
|
|
2
|
+
|
|
3
|
+
`@effindomv2/fui-rs` is available under a separate commercial license if you
|
|
4
|
+
want to use it without the obligations of AGPL-3.0-only.
|
|
5
|
+
|
|
6
|
+
To discuss commercial licensing, contact the repository maintainer through the
|
|
7
|
+
project's public contact channel.
|
package/Cargo.toml
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "fui-rs"
|
|
3
|
+
version = "0.1.3"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
license = "AGPL-3.0-only OR LicenseRef-EffinDom-Commercial"
|
|
6
|
+
|
|
7
|
+
[workspace]
|
|
8
|
+
members = [
|
|
9
|
+
".",
|
|
10
|
+
"crates/demo-home",
|
|
11
|
+
"crates/demo-shared",
|
|
12
|
+
"crates/demo-stage4",
|
|
13
|
+
"crates/demo-stage5",
|
|
14
|
+
"crates/demo-worker",
|
|
15
|
+
"crates/demo-workbench",
|
|
16
|
+
]
|
|
17
|
+
resolver = "2"
|
|
18
|
+
|
|
19
|
+
[features]
|
|
20
|
+
default = []
|
|
21
|
+
worker-runtime = []
|
|
22
|
+
native-runtime = []
|
|
23
|
+
|
|
24
|
+
[dependencies]
|
|
25
|
+
|
|
26
|
+
[profile.release]
|
|
27
|
+
opt-level = 3
|
|
28
|
+
lto = true
|
|
29
|
+
codegen-units = 1
|
|
30
|
+
panic = "abort"
|
|
31
|
+
|
|
32
|
+
[lib]
|
|
33
|
+
name = "fui"
|
|
34
|
+
crate-type = ["rlib"]
|
package/LICENSE.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# FUI-RS - Rust SDK for EffinDom v2
|
|
2
|
+
|
|
3
|
+
FUI-RS is the Rust retained-mode SDK for building EffinDom v2 WebAssembly UI
|
|
4
|
+
apps. It provides retained controls, layout nodes, text input, overlays,
|
|
5
|
+
custom drawing, host services, workers, routing support, and app lifecycle
|
|
6
|
+
macros for browser-hosted Rust WASM apps.
|
|
7
|
+
|
|
8
|
+
## Quickstart
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
# Prerequisites: Rust + wasm32-unknown-unknown target + Binaryen
|
|
12
|
+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
|
13
|
+
rustup target add wasm32-unknown-unknown
|
|
14
|
+
brew install binaryen
|
|
15
|
+
|
|
16
|
+
# Clone and build
|
|
17
|
+
git clone https://github.com/zion-sati/fui-rs.git
|
|
18
|
+
cd fui-rs
|
|
19
|
+
npm ci
|
|
20
|
+
npm run build
|
|
21
|
+
npm run serve
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Open:
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
http://127.0.0.1:8080/v2/fui-rs/demo/index.html
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Full quickstart: [docs/v2/fui-rs/QUICKSTART.md](../../docs/v2/fui-rs/QUICKSTART.md)
|
|
31
|
+
|
|
32
|
+
## Minimal app
|
|
33
|
+
|
|
34
|
+
```rust
|
|
35
|
+
use fui::prelude::*;
|
|
36
|
+
|
|
37
|
+
fn build_page() -> FlexBox {
|
|
38
|
+
ui! {
|
|
39
|
+
column().fill_size().padding(24.0, 24.0, 24.0, 24.0) {
|
|
40
|
+
text("Hello from Rust"),
|
|
41
|
+
button("Click me").on_click(|_| logger::info("App", "clicked")),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fui_app!(FlexBox, build_page);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## SDK docs
|
|
50
|
+
|
|
51
|
+
- [SDK docs index](../../docs/v2/fui-rs/SDK_INDEX.md)
|
|
52
|
+
- [API reference](../../docs/v2/fui-rs/API_REFERENCE.md)
|
|
53
|
+
- [Controls and nodes](../../docs/v2/fui-rs/CONTROLS_AND_NODES.md)
|
|
54
|
+
- [Events and callbacks](../../docs/v2/fui-rs/EVENTS_AND_CALLBACKS.md)
|
|
55
|
+
- [Text input reference](../../docs/v2/fui-rs/TEXT_INPUT_REFERENCE.md)
|
|
56
|
+
- [Forms and autofill](../../docs/v2/fui-rs/FORMS_AND_AUTOFILL.md)
|
|
57
|
+
- [Theming and style matrix](../../docs/v2/fui-rs/THEMING_STYLE_MATRIX.md)
|
|
58
|
+
|
|
59
|
+
## What is included
|
|
60
|
+
|
|
61
|
+
| Area | Status |
|
|
62
|
+
|---|---|
|
|
63
|
+
| Retained app lifecycle macros | Available |
|
|
64
|
+
| `ui!` mixed child tree macro | Available |
|
|
65
|
+
| Flex/Grid layout nodes | Available |
|
|
66
|
+
| Text, rich text, image, SVG | Available |
|
|
67
|
+
| Buttons, toggles, slider, dropdown, combobox | Available |
|
|
68
|
+
| TextInput/TextArea | Available |
|
|
69
|
+
| Context menu, popup, dialog, tooltip | Available |
|
|
70
|
+
| Selection, mobile text handles, context toolbar | Available |
|
|
71
|
+
| ScrollView, ScrollBox, VirtualList | Available |
|
|
72
|
+
| Custom drawing and text layouts | Available |
|
|
73
|
+
| Browser file/fetch/worker bridges | Available |
|
|
74
|
+
| Host services/events generator support | Available |
|
|
75
|
+
|
|
76
|
+
## Architecture
|
|
77
|
+
|
|
78
|
+
FUI-RS builds Rust retained UI objects into the EffinDom v2 runtime through the
|
|
79
|
+
browser bridge. Rust app WASM and the UI runtime WASM are separate modules;
|
|
80
|
+
strings and command data cross the bridge through explicit UTF-8/runtime ABI
|
|
81
|
+
calls.
|
|
82
|
+
|
|
83
|
+
Retained controls are cheap clone handles. Cloning a control gives another Rust
|
|
84
|
+
handle to the same retained UI object.
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
AGPL-3.0-only, or commercial license. See [COMMERCIAL.md](COMMERCIAL.md).
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@effindomv2/fui-rs",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"private": false,
|
|
5
|
+
"license": "AGPL-3.0-only OR LicenseRef-EffinDom-Commercial",
|
|
6
|
+
"description": "EffinDom v2 Rust frontend framework SDK and host generators",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=18"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"scripts",
|
|
17
|
+
"Cargo.toml",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE.md",
|
|
20
|
+
"COMMERCIAL.md"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"generate:abi": "mkdir -p build && esbuild ../abi/generate.ts --bundle --format=esm --platform=node --target=node20 --packages=external --outfile=build/generate-abi.mjs && node build/generate-abi.mjs fui-rs-ffi",
|
|
24
|
+
"check:abi": "mkdir -p build && esbuild ../abi/generate.ts --bundle --format=esm --platform=node --target=node20 --packages=external --outfile=build/generate-abi.mjs && node build/generate-abi.mjs fui-rs-ffi --check",
|
|
25
|
+
"generate:host-services": "mkdir -p build && esbuild scripts/generate-host-services.ts --bundle --format=esm --platform=node --target=node20 --packages=external --outfile=build/generate-host-services.mjs && node build/generate-host-services.mjs demo/src/host-services.ts demoHostServices crates/demo-shared/src/generated/host_services.rs fui::host_services",
|
|
26
|
+
"generate:host-events:home": "mkdir -p build && esbuild scripts/generate-host-events.ts --bundle --format=esm --platform=node --target=node20 --packages=external --outfile=build/generate-host-events.mjs && node build/generate-host-events.mjs demo/src/host-events.ts demoHostEvents crates/demo-home/src/generated/host_events.rs",
|
|
27
|
+
"generate:host-events:workbench": "mkdir -p build && esbuild scripts/generate-host-events.ts --bundle --format=esm --platform=node --target=node20 --packages=external --outfile=build/generate-host-events.mjs && node build/generate-host-events.mjs demo/src/host-events.ts demoHostEvents crates/demo-workbench/src/generated/host_events.rs",
|
|
28
|
+
"generate:host-events:stage4": "mkdir -p build && esbuild scripts/generate-host-events.ts --bundle --format=esm --platform=node --target=node20 --packages=external --outfile=build/generate-host-events.mjs && node build/generate-host-events.mjs demo/src/host-events.ts demoHostEvents crates/demo-stage4/src/generated/host_events.rs",
|
|
29
|
+
"generate:host-events:stage5": "mkdir -p build && esbuild scripts/generate-host-events.ts --bundle --format=esm --platform=node --target=node20 --packages=external --outfile=build/generate-host-events.mjs && node build/generate-host-events.mjs demo/src/host-events.ts demoHostEvents crates/demo-stage5/src/generated/host_events.rs",
|
|
30
|
+
"generate:worker-host-services": "mkdir -p build && esbuild scripts/generate-host-services.ts --bundle --format=esm --platform=node --target=node20 --packages=external --outfile=build/generate-host-services.mjs && node build/generate-host-services.mjs demo/src/worker-host-services.ts demoWorkerHostServices crates/demo-shared/src/generated/worker_host_services.rs fui::host_services fui_host_service",
|
|
31
|
+
"generate:framework-host-services": "mkdir -p build && esbuild scripts/generate-host-services.ts --bundle --format=esm --platform=node --target=node20 --packages=external --outfile=build/generate-host-services.mjs && node build/generate-host-services.mjs scripts/framework-host-services.ts frameworkHostServices src/generated/framework_host_services.rs crate::host_services fui_host",
|
|
32
|
+
"generate:host": "npm run generate:host-services && npm run generate:host-events:home && npm run generate:host-events:workbench && npm run generate:host-events:stage4 && npm run generate:host-events:stage5 && npm run generate:worker-host-services && npm run generate:framework-host-services",
|
|
33
|
+
"guard:runtime-dependency": "bash scripts/check-runtime-dependency.sh",
|
|
34
|
+
"lint": "cd ../.. && npm run lint:v2",
|
|
35
|
+
"build": "npm run guard:runtime-dependency && npm run check:abi && bash scripts/build.sh",
|
|
36
|
+
"test:unit": "npm run guard:runtime-dependency && npm run check:abi && npm run generate:host && bash -lc 'source \"$HOME/.cargo/env\" && cargo test --manifest-path Cargo.toml'",
|
|
37
|
+
"test:integration": "npm --prefix ../.. run build:v2:browser-bridge && npm run build && playwright test -c playwright.config.ts",
|
|
38
|
+
"typecheck": "npm run guard:runtime-dependency && npm run check:abi && tsc -p tsconfig.json --noEmit",
|
|
39
|
+
"prepublishOnly": "npm run check:abi && npm run lint && npm run typecheck"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@effindomv2/runtime": "0.1.24"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@playwright/test": "1.59.1",
|
|
46
|
+
"@types/node": "^24.10.1",
|
|
47
|
+
"esbuild": "^0.27.7",
|
|
48
|
+
"typescript": "^5.9.3"
|
|
49
|
+
},
|
|
50
|
+
"allowScripts": {
|
|
51
|
+
"esbuild@0.27.7": true,
|
|
52
|
+
"fsevents@2.3.2": true
|
|
53
|
+
}
|
|
54
|
+
}
|
package/scripts/build.sh
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
PACKAGE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
6
|
+
REPO_ROOT="$(cd "${PACKAGE_DIR}/../.." && pwd)"
|
|
7
|
+
OUT_DIR="${REPO_ROOT}/public/v2/fui-rs"
|
|
8
|
+
DEMO_OUT_DIR="${OUT_DIR}/demo"
|
|
9
|
+
PUBLIC_BRIDGE_DIR="${REPO_ROOT}/public/v2/browser-bridge"
|
|
10
|
+
PACKAGE_BRIDGE_DIR="${REPO_ROOT}/v2/browser-bridge/dist"
|
|
11
|
+
SHARED_FONTS_DIR="${REPO_ROOT}/public/v2/fonts"
|
|
12
|
+
SHARED_DEMO_TEXTURE="${REPO_ROOT}/v2/fui-as/demo/demo-texture.png"
|
|
13
|
+
HOST_SERVICE_GENERATOR_BUILD="${PACKAGE_DIR}/build/generate-host-services.mjs"
|
|
14
|
+
HOST_EVENT_GENERATOR_BUILD="${PACKAGE_DIR}/build/generate-host-events.mjs"
|
|
15
|
+
WORKER_BOOTSTRAP_BUILD="${PACKAGE_DIR}/build/worker-bootstrap.js"
|
|
16
|
+
WORKER_BOOTSTRAP_MAP_BUILD="${PACKAGE_DIR}/build/worker-bootstrap.js.map"
|
|
17
|
+
|
|
18
|
+
mkdir -p "${PACKAGE_DIR}/build" "${OUT_DIR}" "${DEMO_OUT_DIR}" "${DEMO_OUT_DIR}/workbench" "${DEMO_OUT_DIR}/stage4" "${DEMO_OUT_DIR}/stage5"
|
|
19
|
+
|
|
20
|
+
cd "${PACKAGE_DIR}"
|
|
21
|
+
if ! command -v cargo >/dev/null 2>&1 && [ -f "${HOME}/.cargo/env" ]; then
|
|
22
|
+
# shellcheck disable=SC1090
|
|
23
|
+
source "${HOME}/.cargo/env"
|
|
24
|
+
fi
|
|
25
|
+
if ! command -v cargo >/dev/null 2>&1; then
|
|
26
|
+
echo "cargo not found. Install Rust and ensure cargo is on PATH." >&2
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
resolve_runtime_dist_dir() {
|
|
31
|
+
local candidate=""
|
|
32
|
+
local public_manifest="${PUBLIC_BRIDGE_DIR}/effindom.v2.manifest.json"
|
|
33
|
+
local package_manifest="${PACKAGE_BRIDGE_DIR}/effindom.v2.manifest.json"
|
|
34
|
+
local candidates=()
|
|
35
|
+
|
|
36
|
+
if [ -n "${EFFINDOM_RUNTIME_DIST_DIR:-}" ]; then
|
|
37
|
+
candidates+=("${EFFINDOM_RUNTIME_DIST_DIR}")
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
candidates+=(
|
|
41
|
+
"${PUBLIC_BRIDGE_DIR}"
|
|
42
|
+
"${PACKAGE_BRIDGE_DIR}"
|
|
43
|
+
"${PACKAGE_DIR}/node_modules/@effindomv2/runtime/dist"
|
|
44
|
+
"${REPO_ROOT}/node_modules/@effindomv2/runtime/dist"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if [ -f "${public_manifest}" ] && [ -f "${package_manifest}" ] && ! cmp -s "${public_manifest}" "${package_manifest}"; then
|
|
48
|
+
echo "Note: using ${PUBLIC_BRIDGE_DIR} for local runtime assets." >&2
|
|
49
|
+
echo " ${PACKAGE_BRIDGE_DIR} is a staged package copy and may be stale." >&2
|
|
50
|
+
echo " For ABI changes, run repo-root ./build.sh (or npm run build:v2:abi)." >&2
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
for candidate in "${candidates[@]}"; do
|
|
54
|
+
if [ -f "${candidate}/bridge.js" ] && [ -f "${candidate}/effindom.v2.manifest.json" ] && [ -d "${candidate}/runtime" ]; then
|
|
55
|
+
printf '%s\n' "${candidate}"
|
|
56
|
+
return 0
|
|
57
|
+
fi
|
|
58
|
+
done
|
|
59
|
+
|
|
60
|
+
echo "Could not locate runtime dist assets." >&2
|
|
61
|
+
echo "Expected one of:" >&2
|
|
62
|
+
echo " - \$EFFINDOM_RUNTIME_DIST_DIR" >&2
|
|
63
|
+
echo " - ${PACKAGE_DIR}/node_modules/@effindomv2/runtime/dist" >&2
|
|
64
|
+
echo " - ${REPO_ROOT}/node_modules/@effindomv2/runtime/dist" >&2
|
|
65
|
+
echo " - ${REPO_ROOT}/public/v2/browser-bridge" >&2
|
|
66
|
+
echo " - ${REPO_ROOT}/v2/browser-bridge/dist" >&2
|
|
67
|
+
echo "Install @effindomv2/runtime or build runtime assets first." >&2
|
|
68
|
+
exit 1
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
RUNTIME_DIST_DIR="$(resolve_runtime_dist_dir)"
|
|
72
|
+
optimize_wasm() {
|
|
73
|
+
local wasm_file="$1"
|
|
74
|
+
|
|
75
|
+
if command -v wasm-opt >/dev/null 2>&1; then
|
|
76
|
+
wasm-opt -O3 --strip-debug --strip-producers "${wasm_file}" -o "${wasm_file}"
|
|
77
|
+
else
|
|
78
|
+
echo "wasm-opt not found; skipping Binaryen speed optimization for ${wasm_file}." >&2
|
|
79
|
+
fi
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
generate_host_services() {
|
|
83
|
+
local definition_file="$1"
|
|
84
|
+
local export_name="$2"
|
|
85
|
+
local output_file="$3"
|
|
86
|
+
local runtime_path="${4:-}"
|
|
87
|
+
local host_import_module="${5:-}"
|
|
88
|
+
|
|
89
|
+
npx esbuild "${PACKAGE_DIR}/scripts/generate-host-services.ts" \
|
|
90
|
+
--bundle \
|
|
91
|
+
--format=esm \
|
|
92
|
+
--platform=node \
|
|
93
|
+
--target=node20 \
|
|
94
|
+
--packages=external \
|
|
95
|
+
--outfile="${HOST_SERVICE_GENERATOR_BUILD}"
|
|
96
|
+
|
|
97
|
+
if [ -n "${runtime_path}" ] && [ -n "${host_import_module}" ]; then
|
|
98
|
+
node "${HOST_SERVICE_GENERATOR_BUILD}" \
|
|
99
|
+
"${definition_file}" "${export_name}" "${output_file}" "${runtime_path}" "${host_import_module}"
|
|
100
|
+
elif [ -n "${runtime_path}" ]; then
|
|
101
|
+
node "${HOST_SERVICE_GENERATOR_BUILD}" "${definition_file}" "${export_name}" "${output_file}" "${runtime_path}"
|
|
102
|
+
elif [ -n "${host_import_module}" ]; then
|
|
103
|
+
node "${HOST_SERVICE_GENERATOR_BUILD}" "${definition_file}" "${export_name}" "${output_file}" "" "${host_import_module}"
|
|
104
|
+
else
|
|
105
|
+
node "${HOST_SERVICE_GENERATOR_BUILD}" "${definition_file}" "${export_name}" "${output_file}"
|
|
106
|
+
fi
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
generate_host_events() {
|
|
110
|
+
local definition_file="$1"
|
|
111
|
+
local export_name="$2"
|
|
112
|
+
local output_file="$3"
|
|
113
|
+
|
|
114
|
+
npx esbuild "${PACKAGE_DIR}/scripts/generate-host-events.ts" \
|
|
115
|
+
--bundle \
|
|
116
|
+
--format=esm \
|
|
117
|
+
--platform=node \
|
|
118
|
+
--target=node20 \
|
|
119
|
+
--packages=external \
|
|
120
|
+
--outfile="${HOST_EVENT_GENERATOR_BUILD}"
|
|
121
|
+
|
|
122
|
+
node "${HOST_EVENT_GENERATOR_BUILD}" "${definition_file}" "${export_name}" "${output_file}"
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
generate_host_services "demo/src/host-services.ts" "demoHostServices" "crates/demo-shared/src/generated/host_services.rs" "fui::host_services"
|
|
126
|
+
generate_host_events "demo/src/host-events.ts" "demoHostEvents" "crates/demo-home/src/generated/host_events.rs"
|
|
127
|
+
generate_host_events "demo/src/host-events.ts" "demoHostEvents" "crates/demo-workbench/src/generated/host_events.rs"
|
|
128
|
+
generate_host_events "demo/src/host-events.ts" "demoHostEvents" "crates/demo-stage4/src/generated/host_events.rs"
|
|
129
|
+
generate_host_events "demo/src/host-events.ts" "demoHostEvents" "crates/demo-stage5/src/generated/host_events.rs"
|
|
130
|
+
generate_host_services "demo/src/worker-host-services.ts" "demoWorkerHostServices" "crates/demo-shared/src/generated/worker_host_services.rs" "fui::host_services" "fui_host_service"
|
|
131
|
+
generate_host_services "demo/src/worker-host-services.ts" "demoWorkerHostServices" "crates/demo-worker/src/generated/worker_host_services.rs" "" "fui_host_service"
|
|
132
|
+
generate_host_services "scripts/framework-host-services.ts" "frameworkHostServices" "src/generated/framework_host_services.rs" "crate::host_services" "fui_host"
|
|
133
|
+
|
|
134
|
+
rm -f "${OUT_DIR}/app.wasm" "${OUT_DIR}/harness.js" "${OUT_DIR}/harness.js.map" "${DEMO_OUT_DIR}/demo.wasm"
|
|
135
|
+
|
|
136
|
+
build_demo_route() {
|
|
137
|
+
local package="$1"
|
|
138
|
+
local wasm_name="$2"
|
|
139
|
+
local wasm_out="$3"
|
|
140
|
+
|
|
141
|
+
cargo build --package "${package}" --target wasm32-unknown-unknown --target-dir "${PACKAGE_DIR}/target" --release
|
|
142
|
+
cp "${PACKAGE_DIR}/target/wasm32-unknown-unknown/release/${wasm_name}.wasm" "${wasm_out}"
|
|
143
|
+
optimize_wasm "${wasm_out}"
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
build_demo_route "fui-rs-demo-home" "fui_rs_demo_home" "${DEMO_OUT_DIR}/home.wasm"
|
|
147
|
+
build_demo_route "fui-rs-demo-workbench" "fui_rs_demo_workbench" "${DEMO_OUT_DIR}/workbench.wasm"
|
|
148
|
+
build_demo_route "fui-rs-demo-stage4" "fui_rs_demo_stage4" "${DEMO_OUT_DIR}/stage4.wasm"
|
|
149
|
+
build_demo_route "fui-rs-demo-stage5" "fui_rs_demo_stage5" "${DEMO_OUT_DIR}/stage5.wasm"
|
|
150
|
+
build_demo_route "fui-rs-demo-worker" "fui_rs_demo_worker" "${DEMO_OUT_DIR}/workers.wasm"
|
|
151
|
+
cp "${DEMO_OUT_DIR}/workers.wasm" "${DEMO_OUT_DIR}/workbench/workers.wasm"
|
|
152
|
+
cp "${DEMO_OUT_DIR}/workers.wasm" "${DEMO_OUT_DIR}/stage4/workers.wasm"
|
|
153
|
+
cp "${DEMO_OUT_DIR}/workers.wasm" "${DEMO_OUT_DIR}/stage5/workers.wasm"
|
|
154
|
+
|
|
155
|
+
npx esbuild "${PACKAGE_DIR}/demo/harness.ts" \
|
|
156
|
+
--bundle \
|
|
157
|
+
--format=esm \
|
|
158
|
+
--platform=browser \
|
|
159
|
+
--target=es2020 \
|
|
160
|
+
--minify \
|
|
161
|
+
--outfile="${DEMO_OUT_DIR}/harness.js" \
|
|
162
|
+
--sourcemap
|
|
163
|
+
|
|
164
|
+
npx esbuild "${PACKAGE_DIR}/demo/worker-host-services.ts" \
|
|
165
|
+
--bundle \
|
|
166
|
+
--format=iife \
|
|
167
|
+
--platform=browser \
|
|
168
|
+
--target=es2020 \
|
|
169
|
+
--minify \
|
|
170
|
+
--outfile="${DEMO_OUT_DIR}/worker-host-services.js" \
|
|
171
|
+
--sourcemap
|
|
172
|
+
|
|
173
|
+
npx esbuild "${REPO_ROOT}/v2/browser-bridge/src/managed-harness/worker-bootstrap.ts" \
|
|
174
|
+
--bundle \
|
|
175
|
+
--format=iife \
|
|
176
|
+
--platform=browser \
|
|
177
|
+
--target=es2020 \
|
|
178
|
+
--minify \
|
|
179
|
+
--outfile="${WORKER_BOOTSTRAP_BUILD}" \
|
|
180
|
+
--sourcemap
|
|
181
|
+
|
|
182
|
+
cp "${WORKER_BOOTSTRAP_BUILD}" "${DEMO_OUT_DIR}/worker-bootstrap.js"
|
|
183
|
+
cp "${WORKER_BOOTSTRAP_MAP_BUILD}" "${DEMO_OUT_DIR}/worker-bootstrap.js.map"
|
|
184
|
+
|
|
185
|
+
cp "${PACKAGE_DIR}/browser/index.html" "${OUT_DIR}/index.html"
|
|
186
|
+
cp "${PACKAGE_DIR}/demo/index.html" "${DEMO_OUT_DIR}/index.html"
|
|
187
|
+
cp "${PACKAGE_DIR}/demo/route-shell.html" "${DEMO_OUT_DIR}/workbench/index.html"
|
|
188
|
+
cp "${PACKAGE_DIR}/demo/route-shell.html" "${DEMO_OUT_DIR}/stage4/index.html"
|
|
189
|
+
cp "${PACKAGE_DIR}/demo/route-shell.html" "${DEMO_OUT_DIR}/stage5/index.html"
|
|
190
|
+
cp "${PACKAGE_DIR}/demo/routes.json" "${DEMO_OUT_DIR}/routes.json"
|
|
191
|
+
rm -f "${DEMO_OUT_DIR}/worker-manifest.json"
|
|
192
|
+
if [ -f "${SHARED_DEMO_TEXTURE}" ]; then
|
|
193
|
+
cp "${SHARED_DEMO_TEXTURE}" "${OUT_DIR}/demo-texture.png"
|
|
194
|
+
cp "${SHARED_DEMO_TEXTURE}" "${DEMO_OUT_DIR}/demo-texture.png"
|
|
195
|
+
cp "${SHARED_DEMO_TEXTURE}" "${DEMO_OUT_DIR}/workbench/demo-texture.png"
|
|
196
|
+
cp "${SHARED_DEMO_TEXTURE}" "${DEMO_OUT_DIR}/stage4/demo-texture.png"
|
|
197
|
+
cp "${SHARED_DEMO_TEXTURE}" "${DEMO_OUT_DIR}/stage5/demo-texture.png"
|
|
198
|
+
fi
|
|
199
|
+
cat > "${OUT_DIR}/effindom-runtime-config.js" << 'RUNTIMECFG'
|
|
200
|
+
window.__effindomRuntime = Object.assign({}, window.__effindomRuntime, {
|
|
201
|
+
manifestUrl: './effindom.v2.manifest.json',
|
|
202
|
+
});
|
|
203
|
+
RUNTIMECFG
|
|
204
|
+
cp "${OUT_DIR}/effindom-runtime-config.js" "${DEMO_OUT_DIR}/effindom-runtime-config.js"
|
|
205
|
+
cp "${RUNTIME_DIST_DIR}/bridge.js" "${OUT_DIR}/bridge.js"
|
|
206
|
+
cp "${RUNTIME_DIST_DIR}/bridge.js.map" "${OUT_DIR}/bridge.js.map"
|
|
207
|
+
cp "${RUNTIME_DIST_DIR}/effindom.v2.manifest.json" "${OUT_DIR}/effindom.v2.manifest.json"
|
|
208
|
+
cp "${OUT_DIR}/bridge.js" "${DEMO_OUT_DIR}/bridge.js"
|
|
209
|
+
cp "${OUT_DIR}/bridge.js.map" "${DEMO_OUT_DIR}/bridge.js.map"
|
|
210
|
+
cp "${OUT_DIR}/effindom.v2.manifest.json" "${DEMO_OUT_DIR}/effindom.v2.manifest.json"
|
|
211
|
+
if [ -f "${RUNTIME_DIST_DIR}/icu-asset.json" ]; then
|
|
212
|
+
cp "${RUNTIME_DIST_DIR}/icu-asset.json" "${OUT_DIR}/icu-asset.json"
|
|
213
|
+
cp "${OUT_DIR}/icu-asset.json" "${DEMO_OUT_DIR}/icu-asset.json"
|
|
214
|
+
fi
|
|
215
|
+
rm -rf "${OUT_DIR}/runtime"
|
|
216
|
+
cp -R "${RUNTIME_DIST_DIR}/runtime" "${OUT_DIR}/runtime"
|
|
217
|
+
rm -rf "${OUT_DIR}/fonts"
|
|
218
|
+
rm -rf "${DEMO_OUT_DIR}/fonts"
|
|
219
|
+
if [ -d "${SHARED_FONTS_DIR}" ]; then
|
|
220
|
+
cp -R "${SHARED_FONTS_DIR}" "${OUT_DIR}/fonts"
|
|
221
|
+
cp -R "${SHARED_FONTS_DIR}" "${DEMO_OUT_DIR}/fonts"
|
|
222
|
+
elif [ -d "${RUNTIME_DIST_DIR}/fonts" ]; then
|
|
223
|
+
cp -R "${RUNTIME_DIST_DIR}/fonts" "${OUT_DIR}/fonts"
|
|
224
|
+
cp -R "${RUNTIME_DIST_DIR}/fonts" "${DEMO_OUT_DIR}/fonts"
|
|
225
|
+
fi
|
|
226
|
+
rm -rf "${DEMO_OUT_DIR}/runtime"
|
|
227
|
+
cp -R "${RUNTIME_DIST_DIR}/runtime" "${DEMO_OUT_DIR}/runtime"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
PACKAGE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
6
|
+
REPO_ROOT="$(cd "${PACKAGE_DIR}/../.." && pwd)"
|
|
7
|
+
PACKAGE_JSON="${PACKAGE_DIR}/package.json"
|
|
8
|
+
RUNTIME_PACKAGE_JSON="${REPO_ROOT}/v2/browser-bridge/package.json"
|
|
9
|
+
|
|
10
|
+
runtime_spec="$(
|
|
11
|
+
node -e '
|
|
12
|
+
const fs = require("node:fs");
|
|
13
|
+
const pkg = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
|
14
|
+
process.stdout.write(String(pkg.dependencies?.["@effindomv2/runtime"] ?? ""));
|
|
15
|
+
' "${PACKAGE_JSON}"
|
|
16
|
+
)"
|
|
17
|
+
|
|
18
|
+
if [ -z "${runtime_spec}" ]; then
|
|
19
|
+
echo "@effindomv2/runtime must be declared in dependencies." >&2
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
if ! printf '%s' "${runtime_spec}" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.]+)?$'; then
|
|
24
|
+
echo "@effindomv2/runtime must be pinned to an exact version, found: ${runtime_spec}" >&2
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
if [ -f "${RUNTIME_PACKAGE_JSON}" ]; then
|
|
29
|
+
runtime_version="$(
|
|
30
|
+
node -e '
|
|
31
|
+
const fs = require("node:fs");
|
|
32
|
+
const pkg = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
|
33
|
+
process.stdout.write(String(pkg.version ?? ""));
|
|
34
|
+
' "${RUNTIME_PACKAGE_JSON}"
|
|
35
|
+
)"
|
|
36
|
+
|
|
37
|
+
if [ -n "${runtime_version}" ] && [ "${runtime_spec}" != "${runtime_version}" ]; then
|
|
38
|
+
echo "@effindomv2/fui-rs depends on @effindomv2/runtime@${runtime_spec}, but the local runtime is ${runtime_version}." >&2
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
fi
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { defineHostServices, hostService } from "../../browser-bridge/src/managed-harness/host-services.js";
|
|
2
|
+
|
|
3
|
+
function unsupported(): never {
|
|
4
|
+
throw new Error("Framework host service definitions are generator metadata only.");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const frameworkHostServices = defineHostServices({
|
|
8
|
+
frameworkHost: {
|
|
9
|
+
nowMs: hostService({
|
|
10
|
+
importName: "fui_now_ms",
|
|
11
|
+
args: [] as const,
|
|
12
|
+
returns: "f64",
|
|
13
|
+
implementation: () => unsupported(),
|
|
14
|
+
}),
|
|
15
|
+
isDarkMode: hostService({
|
|
16
|
+
importName: "fui_is_dark_mode",
|
|
17
|
+
args: [] as const,
|
|
18
|
+
returns: "bool",
|
|
19
|
+
implementation: () => unsupported(),
|
|
20
|
+
}),
|
|
21
|
+
accentColor: hostService({
|
|
22
|
+
importName: "fui_get_accent_color",
|
|
23
|
+
args: [] as const,
|
|
24
|
+
returns: "u32",
|
|
25
|
+
implementation: () => unsupported(),
|
|
26
|
+
}),
|
|
27
|
+
platformFamily: hostService({
|
|
28
|
+
importName: "fui_get_platform_family",
|
|
29
|
+
args: [] as const,
|
|
30
|
+
returns: "u32",
|
|
31
|
+
implementation: () => unsupported(),
|
|
32
|
+
}),
|
|
33
|
+
isCoarsePointer: hostService({
|
|
34
|
+
importName: "fui_is_coarse_pointer",
|
|
35
|
+
args: [] as const,
|
|
36
|
+
returns: "bool",
|
|
37
|
+
implementation: () => unsupported(),
|
|
38
|
+
}),
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { generateRustHostEventsFile } from "./hostgen/rust-host-events";
|
|
3
|
+
|
|
4
|
+
async function main(): Promise<void> {
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
if (args.length < 3) {
|
|
7
|
+
throw new Error("Usage: generate-host-events <module-path> <export-name> <output-path>");
|
|
8
|
+
}
|
|
9
|
+
const [moduleArg, exportName, outputArg] = args as [string, string, string];
|
|
10
|
+
await generateRustHostEventsFile(
|
|
11
|
+
path.resolve(process.cwd(), moduleArg),
|
|
12
|
+
exportName,
|
|
13
|
+
path.resolve(process.cwd(), outputArg),
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
await main();
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { generateRustHostServicesFile } from "./hostgen/rust-host-services";
|
|
3
|
+
|
|
4
|
+
async function main(): Promise<void> {
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
if (args.length < 3) {
|
|
7
|
+
throw new Error(
|
|
8
|
+
"Usage: generate-host-services <module-path> <export-name> <output-path> [runtime-path] [host-import-module]",
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
const [moduleArg, exportName, outputArg, runtimePathArg, hostModuleArg] = args as [
|
|
12
|
+
string,
|
|
13
|
+
string,
|
|
14
|
+
string,
|
|
15
|
+
string | undefined,
|
|
16
|
+
string | undefined,
|
|
17
|
+
];
|
|
18
|
+
await generateRustHostServicesFile(
|
|
19
|
+
path.resolve(process.cwd(), moduleArg),
|
|
20
|
+
exportName,
|
|
21
|
+
path.resolve(process.cwd(), outputArg),
|
|
22
|
+
runtimePathArg,
|
|
23
|
+
hostModuleArg ?? "fui_host_service",
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
await main();
|
|
28
|
+
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { build } from "esbuild";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
export function toPosix(value: string): string {
|
|
8
|
+
return value.split(path.sep).join("/");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function relativeImport(fromFile: string, targetFile: string): string {
|
|
12
|
+
let relative = path.relative(path.dirname(fromFile), targetFile);
|
|
13
|
+
relative = relative.replace(/\.[^.]+$/, "");
|
|
14
|
+
if (!relative.startsWith(".")) {
|
|
15
|
+
relative = `./${relative}`;
|
|
16
|
+
}
|
|
17
|
+
return toPosix(relative);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function sourcePathForHeader(sourceModulePath: string): string {
|
|
21
|
+
const relative = path.relative(process.cwd(), sourceModulePath);
|
|
22
|
+
return toPosix(relative.startsWith(".") ? relative : `./${relative}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function loadModuleExport(
|
|
26
|
+
modulePath: string,
|
|
27
|
+
exportName: string,
|
|
28
|
+
tempPrefix: string,
|
|
29
|
+
): Promise<unknown> {
|
|
30
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), tempPrefix));
|
|
31
|
+
const bundledFile = path.join(tempDir, "module.mjs");
|
|
32
|
+
try {
|
|
33
|
+
await build({
|
|
34
|
+
entryPoints: [modulePath],
|
|
35
|
+
outfile: bundledFile,
|
|
36
|
+
bundle: true,
|
|
37
|
+
format: "esm",
|
|
38
|
+
platform: "node",
|
|
39
|
+
target: "node20",
|
|
40
|
+
logLevel: "silent",
|
|
41
|
+
});
|
|
42
|
+
const loaded = await import(pathToFileURL(bundledFile).href) as Record<string, unknown>;
|
|
43
|
+
if (!(exportName in loaded)) {
|
|
44
|
+
throw new Error(`Module does not export "${exportName}".`);
|
|
45
|
+
}
|
|
46
|
+
return loaded[exportName];
|
|
47
|
+
} finally {
|
|
48
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function snakeCaseIdentifier(value: string): string {
|
|
53
|
+
let result = "";
|
|
54
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
55
|
+
const char = value[index] ?? "";
|
|
56
|
+
const previous = index > 0 ? value[index - 1] ?? "" : "";
|
|
57
|
+
const next = index + 1 < value.length ? value[index + 1] ?? "" : "";
|
|
58
|
+
const isUpper = char >= "A" && char <= "Z";
|
|
59
|
+
const prevIsLowerOrDigit =
|
|
60
|
+
(previous >= "a" && previous <= "z") || (previous >= "0" && previous <= "9");
|
|
61
|
+
const nextIsLower = next >= "a" && next <= "z";
|
|
62
|
+
if (
|
|
63
|
+
index > 0 &&
|
|
64
|
+
isUpper &&
|
|
65
|
+
(prevIsLowerOrDigit || ((previous >= "A" && previous <= "Z") && nextIsLower))
|
|
66
|
+
) {
|
|
67
|
+
result += "_";
|
|
68
|
+
}
|
|
69
|
+
result += char.toLowerCase();
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|