Skip to content

Commit 1a16732

Browse files
committed
Add formatting functions and examples
1 parent c478728 commit 1a16732

6 files changed

Lines changed: 489 additions & 14 deletions

File tree

README.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -136,31 +136,30 @@ URL string and to process the components:
136136
```c++
137137
// url_parts.cpp
138138

139+
#include <print>
140+
139141
#include <skyr/url.hpp>
140-
#include <skyr/percent_encoding/percent_decode.hpp>
141-
#include <iostream>
142+
#include <skyr/url_format.hpp>
142143

143144
int main() {
144145
using namespace skyr::literals;
145146

146147
auto url =
147148
"http://sub.example.إختبار:8090/\xcf\x80?a=1&c=2&b=\xe2\x80\x8d\xf0\x9f\x8c\x88"_url;
148149

149-
std::cout << "Protocol: " << url.protocol() << std::endl;
150-
151-
std::cout << "Domain? " << std::boolalpha << url.is_domain() << std::endl;
152-
std::cout << "Domain: " << url.hostname() << std::endl;
153-
std::cout << "Domain: " << url.u8domain().value() << std::endl;
154-
155-
std::cout << "Port: " << url.port<std::uint16_t>().value() << std::endl;
156150

157-
std::cout << "Pathname: "
158-
<< skyr::percent_decode(url.pathname()).value() << std::endl;
151+
std::println("Origin: {:o}", url);
152+
std::println("Protocol: {:s}", url);
153+
std::println("Domain? {}", url.is_domain());
154+
std::println("Domain: {:h}", url); // Encoded (punycode)
155+
std::println("Domain: {:hd}", url); // Decoded (unicode)
156+
std::println("Port: {:p}", url);
157+
std::println("Pathname: {:Pd}", url); // Decoded pathname
159158

160-
std::cout << "Search parameters:" << std::endl;
159+
std::println("Search parameters:");
161160
const auto &search = url.search_parameters();
162161
for (const auto &[key, value] : search) {
163-
std::cout << " " << "key: " << key << ", value = " << value << std::endl;
162+
std::println(" key: {}, value = {}", key, value);
164163
}
165164
}
166165
```
@@ -189,7 +188,8 @@ target_link_libraries(url_parts PRIVATE skyr::skyr-url)
189188
The output of this program is:
190189

191190
```bash
192-
Protocol: http:
191+
Origin: http://sub.example.xn--kgbechtv:8090
192+
Protocol: http
193193
Domain? true
194194
Domain: sub.example.xn--kgbechtv
195195
Domain: sub.example.إختبار

examples/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ foreach(
1919
example_10.cpp
2020
example_11.cpp
2121
example_12.cpp
22+
example_13.cpp
2223
)
2324
skyr_remove_extension(${file_name} example)
2425
add_executable(${example} ${file_name})

examples/example_13.cpp

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright 2025 Glyn Matthews.
2+
// Distributed under the Boost Software License, Version 1.0.
3+
// (See accompanying file LICENSE_1_0.txt of copy at
4+
// http://www.boost.org/LICENSE_1_0.txt)
5+
6+
#include <print>
7+
8+
#include <skyr/url.hpp>
9+
#include <skyr/url_format.hpp>
10+
11+
int main() {
12+
auto url = skyr::url("https://user:pass@api.example.com:8080/v1/users?filter=active&limit=10#results");
13+
14+
std::println("URL Formatting Examples:");
15+
std::println("========================\n");
16+
17+
// Full URL (default)
18+
std::println("Full URL: {}", url);
19+
std::println();
20+
21+
// Individual components (encoded by default)
22+
std::println("Scheme: {:s}", url);
23+
std::println("Hostname: {:h}", url);
24+
std::println("Port: {:p}", url);
25+
std::println("Pathname: {:P}", url);
26+
std::println("Query: {:q}", url);
27+
std::println("Fragment: {:f}", url);
28+
std::println("Origin: {:o}", url);
29+
std::println();
30+
31+
// Practical examples
32+
std::println("API Endpoint: {:o}{:P}", url, url);
33+
std::println("Host:Port: {:h}:{:p}", url, url);
34+
std::println();
35+
36+
// Different URL types
37+
auto https_url = skyr::url("https://example.com/secure");
38+
auto http_url = skyr::url("http://example.org:3000/api");
39+
auto file_url = skyr::url("file:///Users/test/document.txt");
40+
41+
std::println("HTTPS (default port): {:o}", https_url);
42+
std::println("HTTP (custom port): {:o}", http_url);
43+
std::println("File path: {:P}", file_url);
44+
std::println();
45+
46+
// Logging examples
47+
std::println("Logging Examples:");
48+
std::println("-----------------");
49+
std::println("[INFO] Connecting to {:h}:{:p}", url, url);
50+
std::println("[DEBUG] Request path: {:P}{:q}", url, url);
51+
std::println("[TRACE] Full endpoint: {:o}{:P}", url, url);
52+
std::println();
53+
54+
// IP addresses
55+
auto ipv4_url = skyr::url("http://192.168.1.1:8080/admin");
56+
auto ipv6_url = skyr::url("http://[2001:db8::1]:8080/api");
57+
58+
std::println("IPv4: {:h}:{:p}", ipv4_url, ipv4_url);
59+
std::println("IPv6: {:h}:{:p}", ipv6_url, ipv6_url);
60+
std::println();
61+
62+
// Decoded output examples
63+
auto encoded_url = skyr::url("http://example.إختبار/hello%20world?name=John%20Doe&city=Paisley#section%201");
64+
65+
std::println("Encoded vs Decoded:");
66+
std::println("-------------------");
67+
std::println("Hostname (encoded): {:h}", encoded_url);
68+
std::println("Hostname (decoded): {:hd}", encoded_url);
69+
std::println();
70+
std::println("Pathname (encoded): {:P}", encoded_url);
71+
std::println("Pathname (decoded): {:Pd}", encoded_url);
72+
std::println();
73+
std::println("Query (encoded): {:q}", encoded_url);
74+
std::println("Query (decoded): {:qd}", encoded_url);
75+
std::println();
76+
std::println("Fragment (encoded): {:f}", encoded_url);
77+
std::println("Fragment (decoded): {:fd}", encoded_url);
78+
std::println();
79+
80+
// Human-readable logging
81+
std::println("Human-Readable Logging:");
82+
std::println("-----------------------");
83+
std::println("[INFO] User requested: {:Pd}", encoded_url);
84+
std::println("[DEBUG] Query params: {:qd}", encoded_url);
85+
std::println("[TRACE] Host: {:hd}, Path: {:Pd}", encoded_url, encoded_url);
86+
87+
return 0;
88+
}

include/skyr/url_format.hpp

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
// Copyright 2025 Glyn Matthews.
2+
// Distributed under the Boost Software License, Version 1.0.
3+
// (See accompanying file LICENSE_1_0.txt of copy at
4+
// http://www.boost.org/LICENSE_1_0.txt)
5+
6+
#ifndef SKYR_URL_FORMAT_HPP
7+
#define SKYR_URL_FORMAT_HPP
8+
9+
#include <format>
10+
11+
#include <skyr/percent_encoding/percent_decode.hpp>
12+
#include <skyr/url.hpp>
13+
14+
/// \file url_format.hpp
15+
/// Provides std::format support for skyr::url
16+
///
17+
/// Format specifications:
18+
/// - {} or {:} - Full URL (default)
19+
/// - {:s} - Scheme (without ':')
20+
/// - {:h} - Hostname (punycode/ASCII)
21+
/// - {:hd} - Hostname (unicode decoded)
22+
/// - {:p} - Port (empty if default)
23+
/// - {:P} - Pathname (percent-encoded)
24+
/// - {:Pd} - Pathname (percent-decoded)
25+
/// - {:q} - Search/query (percent-encoded, with '?')
26+
/// - {:qd} - Search/query (percent-decoded)
27+
/// - {:f} - Fragment (percent-encoded, with '#')
28+
/// - {:fd} - Fragment (percent-decoded)
29+
/// - {:o} - Origin (scheme://host:port)
30+
///
31+
/// The 'd' modifier decodes percent-encoding and punycode.
32+
/// If decoding fails, falls back to the encoded version.
33+
///
34+
/// Examples:
35+
/// \code
36+
/// auto url = skyr::url("http://example.إختبار/π?name=John%20Doe");
37+
/// std::println("{:h}", url); // example.xn--kgbechtv (punycode)
38+
/// std::println("{:hd}", url); // example.إختبار (unicode)
39+
/// std::println("{:P}", url); // /%CF%80 (encoded)
40+
/// std::println("{:Pd}", url); // /π (decoded)
41+
/// std::println("{:q}", url); // ?name=John%20Doe (encoded)
42+
/// std::println("{:qd}", url); // ?name=John Doe (decoded)
43+
/// \endcode
44+
45+
template <>
46+
struct std::formatter<skyr::url> {
47+
enum class format_type {
48+
full, // Full URL (default)
49+
scheme, // s - scheme
50+
hostname, // h - hostname
51+
port, // p - port
52+
pathname, // P - pathname
53+
query, // q - query/search
54+
fragment, // f - fragment
55+
origin // o - origin
56+
};
57+
58+
format_type type_ = format_type::full;
59+
bool decode_ = false; // 'd' modifier for decoded output
60+
61+
constexpr auto parse(std::format_parse_context& ctx) {
62+
auto it = ctx.begin();
63+
const auto end = ctx.end();
64+
65+
if (it == end || *it == '}') {
66+
type_ = format_type::full;
67+
decode_ = false;
68+
return it;
69+
}
70+
71+
// Parse format spec
72+
switch (*it) {
73+
case 's':
74+
type_ = format_type::scheme;
75+
++it;
76+
break;
77+
case 'h':
78+
type_ = format_type::hostname;
79+
++it;
80+
break;
81+
case 'p':
82+
type_ = format_type::port;
83+
++it;
84+
break;
85+
case 'P':
86+
type_ = format_type::pathname;
87+
++it;
88+
break;
89+
case 'q':
90+
type_ = format_type::query;
91+
++it;
92+
break;
93+
case 'f':
94+
type_ = format_type::fragment;
95+
++it;
96+
break;
97+
case 'o':
98+
type_ = format_type::origin;
99+
++it;
100+
break;
101+
default:
102+
throw std::format_error("Invalid format specifier for skyr::url");
103+
}
104+
105+
// Check for 'd' (decode) modifier
106+
if (it != end && *it == 'd') {
107+
decode_ = true;
108+
++it;
109+
}
110+
111+
if (it != end && *it != '}') {
112+
throw std::format_error("Invalid format specifier for skyr::url");
113+
}
114+
115+
return it;
116+
}
117+
118+
auto format(const skyr::url& url, std::format_context& ctx) const {
119+
switch (type_) {
120+
case format_type::full:
121+
return std::format_to(ctx.out(), "{}", url.href());
122+
123+
case format_type::scheme:
124+
return std::format_to(ctx.out(), "{}", url.scheme());
125+
126+
case format_type::hostname:
127+
if (decode_) {
128+
// Try to get unicode domain, fall back to ASCII if not available
129+
if (auto domain = url.u8domain()) {
130+
return std::format_to(ctx.out(), "{}", domain.value());
131+
}
132+
}
133+
return std::format_to(ctx.out(), "{}", url.hostname());
134+
135+
case format_type::port:
136+
return std::format_to(ctx.out(), "{}", url.port());
137+
138+
case format_type::pathname: {
139+
auto pathname = url.pathname();
140+
if (decode_) {
141+
// Try to percent-decode, fall back to encoded if decode fails
142+
if (auto decoded = skyr::percent_decode(pathname)) {
143+
return std::format_to(ctx.out(), "{}", decoded.value());
144+
}
145+
}
146+
return std::format_to(ctx.out(), "{}", pathname);
147+
}
148+
149+
case format_type::query: {
150+
auto search = url.search();
151+
if (decode_ && !search.empty()) {
152+
// Decode the query string (skip the leading '?')
153+
auto query_part = search.substr(1); // Remove '?'
154+
if (auto decoded = skyr::percent_decode(query_part)) {
155+
return std::format_to(ctx.out(), "?{}", decoded.value());
156+
}
157+
}
158+
return std::format_to(ctx.out(), "{}", search);
159+
}
160+
161+
case format_type::fragment: {
162+
auto hash = url.hash();
163+
if (decode_ && !hash.empty()) {
164+
// Decode the fragment (skip the leading '#')
165+
auto fragment_part = hash.substr(1); // Remove '#'
166+
if (auto decoded = skyr::percent_decode(fragment_part)) {
167+
return std::format_to(ctx.out(), "#{}", decoded.value());
168+
}
169+
}
170+
return std::format_to(ctx.out(), "{}", hash);
171+
}
172+
173+
case format_type::origin:
174+
return std::format_to(ctx.out(), "{}", url.origin());
175+
176+
default:
177+
return ctx.out();
178+
}
179+
}
180+
};
181+
182+
#endif // SKYR_URL_FORMAT_HPP

tests/skyr/url/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ foreach (file_name
99
url_setter_tests.cpp
1010
url_search_parameters_tests.cpp
1111
url_sanitize_tests.cpp
12+
url_format_tests.cpp
1213
wpt_conformance_tests.cpp
1314
)
1415
skyr_create_test(${file_name} ${PROJECT_BINARY_DIR}/tests/url test_name)

0 commit comments

Comments
 (0)