173 lines
6.9 KiB
C++
173 lines
6.9 KiB
C++
//SPDX-License-Identifier: LGPL-3.0-only
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
// Router implementation. The popState handler table lives in
|
|
// Crafter.Graphics-Dom.cpp (alongside every other event-kind table);
|
|
// we reach it through the opaque `PopStateRegister`/`PopStateUnregister`
|
|
// helpers it exposes, avoiding any cross-TU duplication of the
|
|
// HandlerTable definition.
|
|
|
|
module;
|
|
module Crafter.Graphics;
|
|
import std;
|
|
|
|
namespace Crafter::DomBindings {
|
|
__attribute__((import_module("env"), import_name("pushState")))
|
|
void PushState(const char* data, std::int32_t dataLength,
|
|
const char* title, std::int32_t titleLength,
|
|
const char* url, std::int32_t urlLength);
|
|
__attribute__((import_module("env"), import_name("addPopStateListener")))
|
|
void AddPopStateListener(std::int32_t id);
|
|
__attribute__((import_module("env"), import_name("removePopStateListener")))
|
|
void RemovePopStateListener(std::int32_t id);
|
|
__attribute__((import_module("env"), import_name("replaceState")))
|
|
void ReplaceState(const char* data, std::int32_t dataLength,
|
|
const char* title, std::int32_t titleLength,
|
|
const char* url, std::int32_t urlLength);
|
|
__attribute__((import_module("env"), import_name("getPathName")))
|
|
const char* GetPathName();
|
|
__attribute__((import_module("env"), import_name("getSearch")))
|
|
const char* GetSearch();
|
|
__attribute__((import_module("env"), import_name("getHash")))
|
|
const char* GetHash();
|
|
__attribute__((import_module("env"), import_name("getHref")))
|
|
const char* GetHref();
|
|
__attribute__((import_module("env"), import_name("navigate")))
|
|
void Navigate(const char* url, std::int32_t urlLength, bool replace);
|
|
|
|
// Defined in Crafter.Graphics-Dom.cpp.
|
|
std::int32_t PopStateRegister(std::function<void()> cb);
|
|
void PopStateUnregister(std::int32_t id);
|
|
}
|
|
|
|
namespace Crafter::Router {
|
|
|
|
void PushState(std::string_view data, std::string_view title, std::string_view url) {
|
|
Crafter::DomBindings::PushState(
|
|
data.data(), static_cast<std::int32_t>(data.size()),
|
|
title.data(), static_cast<std::int32_t>(title.size()),
|
|
url.data(), static_cast<std::int32_t>(url.size()));
|
|
}
|
|
|
|
std::int32_t AddPopStateListener(std::function<void()> callback) {
|
|
std::int32_t id = Crafter::DomBindings::PopStateRegister(std::move(callback));
|
|
Crafter::DomBindings::AddPopStateListener(id);
|
|
return id;
|
|
}
|
|
|
|
void RemovePopStateListener(std::int32_t id) {
|
|
Crafter::DomBindings::RemovePopStateListener(id);
|
|
Crafter::DomBindings::PopStateUnregister(id);
|
|
}
|
|
|
|
void ReplaceState(std::string_view data, std::string_view title, std::string_view url) {
|
|
Crafter::DomBindings::ReplaceState(
|
|
data.data(), static_cast<std::int32_t>(data.size()),
|
|
title.data(), static_cast<std::int32_t>(title.size()),
|
|
url.data(), static_cast<std::int32_t>(url.size()));
|
|
}
|
|
|
|
// The four location accessors share the same shape: the JS side
|
|
// WasmAllocs a NUL-terminated UTF-8 copy, we adopt it into a
|
|
// std::string and free the raw buffer. A null return means the JS
|
|
// allocation failed.
|
|
namespace {
|
|
std::string AdoptJsString(const char* raw) {
|
|
if (!raw) return {};
|
|
std::string out(raw);
|
|
std::free(const_cast<char*>(raw));
|
|
return out;
|
|
}
|
|
}
|
|
|
|
std::string GetPath() {
|
|
return AdoptJsString(Crafter::DomBindings::GetPathName());
|
|
}
|
|
|
|
std::string GetSearch() {
|
|
return AdoptJsString(Crafter::DomBindings::GetSearch());
|
|
}
|
|
|
|
std::string GetHash() {
|
|
return AdoptJsString(Crafter::DomBindings::GetHash());
|
|
}
|
|
|
|
std::string GetHref() {
|
|
return AdoptJsString(Crafter::DomBindings::GetHref());
|
|
}
|
|
|
|
void Navigate(std::string_view url, bool replace) {
|
|
Crafter::DomBindings::Navigate(
|
|
url.data(), static_cast<std::int32_t>(url.size()), replace);
|
|
}
|
|
|
|
// ─── Query-string parsing ─────────────────────────────────────────
|
|
//
|
|
// Pure string work, no JS round-trip. Kept here rather than left to
|
|
// callers because every consumer would otherwise hand-roll percent
|
|
// decoding, and getting `+` vs `%20` wrong is the usual bug.
|
|
|
|
namespace {
|
|
int HexVal(char c) {
|
|
if (c >= '0' && c <= '9') return c - '0';
|
|
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
|
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
std::string PercentDecode(std::string_view in) {
|
|
std::string out;
|
|
out.reserve(in.size());
|
|
for (std::size_t i = 0; i < in.size(); ++i) {
|
|
const char c = in[i];
|
|
if (c == '+') {
|
|
// application/x-www-form-urlencoded encodes space as '+'.
|
|
out.push_back(' ');
|
|
} else if (c == '%' && i + 2 < in.size()) {
|
|
const int hi = HexVal(in[i + 1]);
|
|
const int lo = HexVal(in[i + 2]);
|
|
if (hi >= 0 && lo >= 0) {
|
|
out.push_back(static_cast<char>(hi * 16 + lo));
|
|
i += 2;
|
|
} else {
|
|
// Malformed escape: pass the '%' through rather than
|
|
// dropping input. Lets a stray '%' in a search box
|
|
// survive a round trip.
|
|
out.push_back(c);
|
|
}
|
|
} else {
|
|
out.push_back(c);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
std::vector<std::pair<std::string, std::string>> ParseQuery(std::string_view query) {
|
|
std::vector<std::pair<std::string, std::string>> out;
|
|
if (!query.empty() && query.front() == '?') query.remove_prefix(1);
|
|
while (!query.empty()) {
|
|
const std::size_t amp = query.find('&');
|
|
std::string_view pair = query.substr(0, amp);
|
|
query = (amp == std::string_view::npos) ? std::string_view{}
|
|
: query.substr(amp + 1);
|
|
if (pair.empty()) continue; // tolerate "a=1&&b=2" and a trailing '&'
|
|
const std::size_t eq = pair.find('=');
|
|
if (eq == std::string_view::npos) {
|
|
// Valueless key ("?debug") — present, empty value.
|
|
out.emplace_back(PercentDecode(pair), std::string{});
|
|
} else {
|
|
out.emplace_back(PercentDecode(pair.substr(0, eq)),
|
|
PercentDecode(pair.substr(eq + 1)));
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
std::optional<std::string> QueryGet(std::string_view query, std::string_view key) {
|
|
for (auto& [k, v] : ParseQuery(query)) {
|
|
if (k == key) return v;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
}
|