diff --git a/doc/modules/ROOT/images/AuthorityDiagram.svg b/doc/modules/ROOT/images/AuthorityDiagram.svg
new file mode 100644
index 00000000..2c5ab605
--- /dev/null
+++ b/doc/modules/ROOT/images/AuthorityDiagram.svg
@@ -0,0 +1,154 @@
+
+
+
\ No newline at end of file
diff --git a/doc/modules/ROOT/images/ClassHierarchy.svg b/doc/modules/ROOT/images/ClassHierarchy.svg
new file mode 100644
index 00000000..add6300c
--- /dev/null
+++ b/doc/modules/ROOT/images/ClassHierarchy.svg
@@ -0,0 +1,152 @@
+
+
+
\ No newline at end of file
diff --git a/doc/modules/ROOT/images/HelpCard.svg b/doc/modules/ROOT/images/HelpCard.svg
new file mode 100644
index 00000000..019a2858
--- /dev/null
+++ b/doc/modules/ROOT/images/HelpCard.svg
@@ -0,0 +1,518 @@
+
+
+
\ No newline at end of file
diff --git a/doc/modules/ROOT/images/PartsDiagram.svg b/doc/modules/ROOT/images/PartsDiagram.svg
new file mode 100644
index 00000000..02634734
--- /dev/null
+++ b/doc/modules/ROOT/images/PartsDiagram.svg
@@ -0,0 +1,145 @@
+
+
+
\ No newline at end of file
diff --git a/doc/modules/ROOT/images/repo-logo.png b/doc/modules/ROOT/images/repo-logo.png
new file mode 100644
index 00000000..192ec262
Binary files /dev/null and b/doc/modules/ROOT/images/repo-logo.png differ
diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc
index c29c00e3..f0472b57 100644
--- a/doc/modules/ROOT/nav.adoc
+++ b/doc/modules/ROOT/nav.adoc
@@ -1,22 +1,29 @@
-* xref:quicklook.adoc[Quick Look]
-* xref:urls/index.adoc[URLs]
-** xref:urls/parsing.adoc[Parsing]
-** xref:urls/containers.adoc[Containers]
-** xref:urls/segments.adoc[Segments]
-** xref:urls/params.adoc[Params]
-** xref:urls/normalization.adoc[Normalization]
-** xref:urls/stringtoken.adoc[String Token]
-** xref:urls/percent-encoding.adoc[Percent Encoding]
-** xref:urls/formatting.adoc[Formatting]
-* xref:grammar/index.adoc[Grammar]
-** xref:grammar/rules.adoc[Parse Rules]
-** xref:grammar/charset.adoc[Character Sets]
-** xref:grammar/combinators.adoc[Compound Rules]
-** xref:grammar/range.adoc[Ranges]
-** xref:grammar/rfc3986.adoc[RFC 3986]
-* xref:concepts/index.adoc[Concepts]
-** xref:concepts/CharSet.adoc[CharSet]
-** xref:concepts/Rule.adoc[Rule]
-** xref:concepts/StringToken.adoc[StringToken]
-* xref:examples.adoc[Examples]
-* xref:HelpCard.adoc[Help Card]
\ No newline at end of file
+* xref:quicklook.adoc[]
+* xref:urls/index.adoc[]
+** xref:urls/parsing.adoc[]
+** xref:urls/containers.adoc[]
+** xref:urls/segments.adoc[]
+** xref:urls/params.adoc[]
+** xref:urls/normalization.adoc[]
+** xref:urls/stringtoken.adoc[]
+** xref:urls/percent-encoding.adoc[]
+** xref:urls/formatting.adoc[]
+* xref:grammar/index.adoc[]
+** xref:grammar/rules.adoc[]
+** xref:grammar/charset.adoc[]
+** xref:grammar/combinators.adoc[]
+** xref:grammar/range.adoc[]
+** xref:grammar/rfc3986.adoc[]
+* Concepts
+** xref:concepts/CharSet.adoc[]
+** xref:concepts/Rule.adoc[]
+** xref:concepts/StringToken.adoc[]
+* Examples
+** xref:examples/qrcode.adoc[]
+** xref:examples/finicky.adoc[]
+** xref:examples/mailto.adoc[]
+** xref:examples/magnet-link.adoc[]
+** xref:examples/file-router.adoc[]
+** xref:examples/router.adoc[]
+** xref:examples/sanitize.adoc[]
+* xref:HelpCard.adoc[]
\ No newline at end of file
diff --git a/doc/modules/ROOT/pages/HelpCard.adoc b/doc/modules/ROOT/pages/HelpCard.adoc
index a4924747..a5792746 100644
--- a/doc/modules/ROOT/pages/HelpCard.adoc
+++ b/doc/modules/ROOT/pages/HelpCard.adoc
@@ -8,8 +8,9 @@
//
-// [section:helpcard Help Card]
+[#helpcard]
+= Help Card
-// [$url/images/HelpCard.svg]
+image:HelpCard.svg[]
diff --git a/doc/modules/ROOT/pages/clipboard.adoc b/doc/modules/ROOT/pages/clipboard.adoc
index b44de221..df33b2b0 100644
--- a/doc/modules/ROOT/pages/clipboard.adoc
+++ b/doc/modules/ROOT/pages/clipboard.adoc
@@ -16,125 +16,211 @@
These member functions are available for interacting
with the scheme component of URL containers:
-// [table Scheme Members [
-// [Name]
-// [Description]
-// ][
-// [[link url.ref.boost__urls__url_view_base.has_scheme `has_scheme`]]
-// [
-// Return `true` if a scheme is present.
-// ]
-// ][
-// [[link url.ref.boost__urls__url_view_base.scheme `scheme`]]
-// [
-// Return the scheme as a string.
-// ]
-// ][
-// [[link url.ref.boost__urls__url_view_base.scheme_id `scheme_id`]]
-// [
-// Return the scheme as a
-// [link url.ref.boost__urls__scheme known scheme]
-// constant,
-// [link url.ref.boost__urls__scheme `scheme::unknown`]
-// if the scheme is not well-known, or
-// [link url.ref.boost__urls__scheme `scheme::none`]
-// if no scheme is present.
-// ]
-// ][
-// [[link url.ref.boost__urls__url_base.remove_scheme `remove_scheme`]]
-// [
-// Remove the scheme if present.
-// ]
-// ][
-// [[link url.ref.boost__urls__url_base.set_scheme `set_scheme`]]
-// [
-// Set the scheme to a given string or
-// [link url.ref.boost__urls__scheme known scheme]
-// constant.
-// ]
-// ]]
+[cols="a,a"]
+|===
+// Headers
+|Name|Description
+
+// Row 1, Column 1
+|`has_scheme`
+// Row 1, Column 2
+|
+// Row 1, Column 3
+|Return `true` if a scheme is present.
+// Row 1, Column 4
+|
+
+// Row 2, Column 1
+|`scheme`
+// Row 2, Column 2
+|
+// Row 2, Column 3
+|Return the scheme as a string.
+// Row 2, Column 4
+|
+
+// Row 3, Column 1
+|`scheme_id`
+// Row 3, Column 2
+|
+// Row 3, Column 3
+|Return the scheme as a
+ known scheme
+ constant,
+ `scheme::unknown`
+ if the scheme is not well-known, or
+ `scheme::none`
+ if no scheme is present.
+// Row 3, Column 4
+|
+
+// Row 4, Column 1
+|`remove_scheme`
+// Row 4, Column 2
+|
+// Row 4, Column 3
+|Remove the scheme if present.
+// Row 4, Column 4
+|
+
+// Row 5, Column 1
+|`set_scheme`
+// Row 5, Column 2
+|
+// Row 5, Column 3
+|Set the scheme to a given string or
+ known scheme
+ constant.
+// Row 5, Column 4
+|
+
+|===
-// [table Userinfo Members [
-// [Name]
-// [Description]
-// ][
-// [[link url.ref.boost__urls__url_view_base.has_userinfo `has_userinfo`]]
-// [
-// Return `true` if a userinfo is present.
-// ]
-// ][
-// [[link url.ref.boost__urls__url_view_base.encoded_userinfo `encoded_userinfo`]]
-// [
-// Return the userinfo field as a percent-encoded string.
-// ]
-// ][
-// [[link url.ref.boost__urls__url_view_base.userinfo `userinfo`]]
-// [
-// Return the userinfo field with percent-decoding applied.
-// ]
-// ][
-// [[link url.ref.boost__urls__url_view_base.encoded_user `encoded_user`]]
-// [
-// Return the user field as a percent-encoded string.
-// ]
-// ][
-// [[link url.ref.boost__urls__url_view_base.user `user`]]
-// [
-// Return the user field with percent-decoding applied.
-// ]
-// ][
-// [[link url.ref.boost__urls__url_view_base.has_password `has_password`]]
-// [
-// Return `true` if a password is present.
-// ]
-// ][
-// [[link url.ref.boost__urls__url_view_base.encoded_password `encoded_password`]]
-// [
-// Return the password as a percent-encoded string.
-// ]
-// ][
-// [[link url.ref.boost__urls__url_view_base.password `password`]]
-// [
-// Return the password with percent-decoding applied.
-// ]
-// ][
-// [[link url.ref.boost__urls__url_base.set_user `set_user`]]
-// [
-// Set the user field using a plain string.
-// ]
-// ][
-// [[link url.ref.boost__urls__url_base.remove_password `remove_password`]]
-// [
-// Remove the password field if present.
-// ]
-// ][
-// [[link url.ref.boost__urls__url_base.set_encoded_password `set_encoded_password`]]
-// [
-// Set the password field using a percent-encoded string.
-// ]
-// ][
-// [[link url.ref.boost__urls__url_base.set_password `set_password`]]
-// [
-// Set the password field using a plain string.
-// ]
-// ][
-// [[link url.ref.boost__urls__url_base.remove_userinfo `remove_userinfo`]]
-// [
-// Remove the entire userinfo if present.
-// ]
-// ][
-// [[link url.ref.boost__urls__url_base.set_encoded_userinfo `set_encoded_userinfo`]]
-// [
-// Set the entire userinfo using a percent-encoded string.
-// ]
-// ][
-// [[link url.ref.boost__urls__url_base.set_userinfo `set_userinfo`]]
-// [
-// Set the entire userinfo using a plain string.
-// ]
-// ]]
+
+[cols="a,a"]
+|===
+// Headers
+|Name|Description
+
+// Row 1, Column 1
+|`has_userinfo`
+// Row 1, Column 2
+|
+// Row 1, Column 3
+|Return `true` if a userinfo is present.
+// Row 1, Column 4
+|
+
+// Row 2, Column 1
+|`encoded_userinfo`
+// Row 2, Column 2
+|
+// Row 2, Column 3
+|Return the userinfo field as a percent-encoded string.
+// Row 2, Column 4
+|
+
+// Row 3, Column 1
+|`userinfo`
+// Row 3, Column 2
+|
+// Row 3, Column 3
+|Return the userinfo field with percent-decoding applied.
+// Row 3, Column 4
+|
+
+// Row 4, Column 1
+|`encoded_user`
+// Row 4, Column 2
+|
+// Row 4, Column 3
+|Return the user field as a percent-encoded string.
+// Row 4, Column 4
+|
+
+// Row 5, Column 1
+|`user`
+// Row 5, Column 2
+|
+// Row 5, Column 3
+|Return the user field with percent-decoding applied.
+// Row 5, Column 4
+|
+
+// Row 6, Column 1
+|`has_password`
+// Row 6, Column 2
+|
+// Row 6, Column 3
+|Return `true` if a password is present.
+// Row 6, Column 4
+|
+
+// Row 7, Column 1
+|`encoded_password`
+// Row 7, Column 2
+|
+// Row 7, Column 3
+|Return the password as a percent-encoded string.
+// Row 7, Column 4
+|
+
+// Row 8, Column 1
+|`password`
+// Row 8, Column 2
+|
+// Row 8, Column 3
+|Return the password with percent-decoding applied.
+// Row 8, Column 4
+|
+
+// Row 9, Column 1
+|`set_user`
+// Row 9, Column 2
+|
+// Row 9, Column 3
+|Set the user field using a plain string.
+// Row 9, Column 4
+|
+
+// Row 10, Column 1
+|`remove_password`
+// Row 10, Column 2
+|
+// Row 10, Column 3
+|Remove the password field if present.
+// Row 10, Column 4
+|
+
+// Row 11, Column 1
+|`set_encoded_password`
+// Row 11, Column 2
+|
+// Row 11, Column 3
+|Set the password field using a percent-encoded string.
+// Row 11, Column 4
+|
+
+// Row 12, Column 1
+|`set_password`
+// Row 12, Column 2
+|
+// Row 12, Column 3
+|Set the password field using a plain string.
+// Row 12, Column 4
+|
+
+// Row 13, Column 1
+|`remove_userinfo`
+// Row 13, Column 2
+|
+// Row 13, Column 3
+|Remove the entire userinfo if present.
+// Row 13, Column 4
+|
+
+// Row 14, Column 1
+|`set_encoded_userinfo`
+// Row 14, Column 2
+|
+// Row 14, Column 3
+|Set the entire userinfo using a percent-encoded string.
+// Row 14, Column 4
+|
+
+// Row 15, Column 1
+|`set_userinfo`
+// Row 15, Column 2
+|
+// Row 15, Column 3
+|Set the entire userinfo using a plain string.
+// Row 15, Column 4
+|
+
+|===
+
diff --git a/doc/modules/ROOT/pages/concepts/CharSet.adoc b/doc/modules/ROOT/pages/concepts/CharSet.adoc
index 03a14fc2..404b0c43 100644
--- a/doc/modules/ROOT/pages/concepts/CharSet.adoc
+++ b/doc/modules/ROOT/pages/concepts/CharSet.adoc
@@ -8,19 +8,17 @@
//
-// [section:charset CharSet]
+[#charset]
+= CharSet
-A __CharSet__ is a unary predicate which is invocable with
-this equivalent signature:
+A __CharSet__ is a unary predicate which is invocable with this equivalent signature:
[source,cpp]
----
bool( char ch ) const noexcept;
----
-
-The predicate returns `true` if `ch` is a member of the
-set, or `false` otherwise.
+The predicate returns `true` if `ch` is a member of the set, or `false` otherwise.
== Related Identifiers
@@ -37,74 +35,90 @@ In this table:
* `c` is a value of type `char`
* `first`, `last` are values of type `char const*`
-// [table Valid expressions
-// [[Expression] [Type] [Semantics, Pre/Post-conditions]]
-// [
-// [`t(c)`]
-// [`bool`]
-// [
-// This function returns `true` if `c` is a member of
-// the character set, otherwise it returns `false`.
-// ]
-// ][
-// [
-// ```
-// t.find_if(first,last)
-// ```
-// ]
-// [`char const*`]
-// [
-// This optional member function examines the valid
-// range of characters in `[first, last)` and returns
-// a pointer to the first occurrence of a character
-// which is in the set, or returns `last` if no such
-// character.
-//
-// The implementation of
-// [link url.ref.boost__urls__grammar__find_if `find_if`]
-// calls this function if provided by the character
-// set, allowing optimized or otherwise performant
-// implementations to be developed. If this member
-// function is not provided, a default implementation
-// is used which calls `operator()`.
-// ]
-// ][
-// [
-// ```
-// t.find_if_not(first,last)
-// ```
-// ]
-// [`char const*`]
-// [
-// This optional member function examines the valid
-// range of characters in `[first, last)` and returns
-// a pointer to the first occurrence of a character
-// which is not in the set, or returns `last` if no
-// such character.
-//
-// The implementation of
-// [link url.ref.boost__urls__grammar__find_if_not `find_if_not`]
-// calls this function if provided by the character
-// set, allowing optimized or otherwise performant
-// implementations to be developed. If this member
-// function is not provided, a default implementation
-// is used which calls `operator()`.
-// ]
-// ]]
-//
-// [heading Exemplar]
-//
-// For best results, it is suggested that all constructors and
-// member functions for character sets be marked `constexpr`.
-//
-// [code_charset_1]
-//
-// [heading Models]
-//
-// * [link url.ref.boost__urls__grammar__alnum_chars `alnum_chars`]
-// * [link url.ref.boost__urls__grammar__alpha_chars `alpha_chars`]
-// * [link url.ref.boost__urls__grammar__digit_chars `digit_chars`]
-// * [link url.ref.boost__urls__grammar__hexdig_chars `hexdig_chars`]
-// * [link url.ref.boost__urls__grammar__lut_chars `lut_chars`]
-//
-// [endsect]
\ No newline at end of file
+[cols="a,a,a"]
+|===
+// Headers
+|Expression|Type|Semantics, Pre/Post-conditions
+
+// Row 1, Column 1
+|`t(c)`
+// Row 1, Column 2
+|`bool`
+// Row 1, Column 3
+|This function returns `true` if `c` is a member of
+the character set, otherwise it returns `false`.
+
+// Row 2, Column 1
+|
+[source,cpp]
+----
+t.find_if(first,last)
+----
+
+// Row 2, Column 2
+|`char const*`
+// Row 2, Column 3
+|This optional member function examines the valid range of characters in `// [first, last)` and returns
+a pointer to the first occurrence of a character
+which is in the set, or returns `last` if no such
+character.
+
+The implementation of `grammar::find_if`
+calls this function if provided by the character
+set, allowing optimized or otherwise performant
+implementations to be developed. If this member
+function is not provided, a default implementation
+is used which calls `operator()`.
+
+// Row 3, Column 1
+|
+[source,cpp]
+----
+t.find_if_not(first,last)
+----
+// Row 3, Column 2
+|`char const*`
+// Row 3, Column 3
+|This optional member function examines the valid
+range of characters in `[first, last)` and returns
+a pointer to the first occurrence of a character
+which is not in the set, or returns `last` if no
+such character.
+
+The implementation of `grammar::find_if_not`
+calls this function if provided by the character
+set, allowing optimized or otherwise performant
+implementations to be developed. If this member
+function is not provided, a default implementation
+is used which calls `operator()`.
+|===
+
+== Exemplar
+
+For best results, it is suggested that all constructors and
+member functions for character sets be marked `constexpr`.
+
+// code_charset_1
+[source,cpp]
+----
+struct CharSet
+{
+ bool operator()( char c ) const noexcept;
+
+ // These are both optional. If either or both are left
+ // unspecified, a default implementation will be used.
+ //
+ char const* find_if( char const* first, char const* last ) const noexcept;
+ char const* find_if_not( char const* first, char const* last ) const noexcept;
+};
+----
+
+== Models
+
+* `grammar::alnum_chars`
+* `grammar::alpha_chars`
+* `grammar::digit_chars`
+* `grammar::hexdig_chars`
+* `grammar::lut_chars`
+
+
diff --git a/doc/modules/ROOT/pages/concepts/Rule.adoc b/doc/modules/ROOT/pages/concepts/Rule.adoc
index 701a29dc..aa49342b 100644
--- a/doc/modules/ROOT/pages/concepts/Rule.adoc
+++ b/doc/modules/ROOT/pages/concepts/Rule.adoc
@@ -8,7 +8,7 @@
//
-== Rule
+= Rule
A __Rule__ defines an algorithm used to match an input buffer of
ASCII characters against a set of syntactical specifications.
@@ -18,11 +18,11 @@ rules for productions typically found in RFC documents. Rules
are not invoked directly; instead, rule variables are used with
overloads of `parse` which provide a convenient, uniform front end.
-=== Related Identifiers
+== Related Identifiers
`is_rule`, `parse`.
-=== Requirements
+== Requirements
In this table:
@@ -31,79 +31,84 @@ In this table:
* `it` is an __lvalue__ with type `char const*`
* `end` is a value of type `char const*`
-// [table Valid expressions
-// [[Expression] [Type] [Semantics, Pre/Post-conditions]]
-// [
-// [
-// ```
-// T(t)
-// ```
-// ]
-// []
-// [
-// Copy construction of `T` throws nothing.
-//
-// `std::is_nothrow_copy_constructible::value == true`
-// ]
-// ][
-// [
-// ```
-// T::value_type
-// ```
-// ]
-// []
-// [
-// Values of this type are returned by the rule when the
-// parse operation is successful
-// ]
-// ][
-// [
-// ```
-// t.parse(it,end)
-// ```
-// ]
-// [`result`]
-// [
-// Attempt to parse the buffer of characters defined by
-// the range `[it,end)`. Upon success, the return result
-// holds an instance of the rule's value type, and
-// the reference parameter `it` is modified to point
-// to the first unconsumed character. Otherwise, upon
-// failure the result holds an error. In this case
-// the implementation defines if and how the reference
-// parameter `it` is modified.
-// ]
-// ]]
-//
-// [heading Exemplar]
-//
-// For best results, it is suggested that all constructors for
-// rules be marked `constexpr`.
-//
-// ```
-// struct Rule
-// {
-// struct value_type;
-//
-// constexpr Rule( Rule const& ) noexcept = default;
-//
-// auto parse( char const*& it, char const* end ) const -> result< value_type >;
-// };
-//
-// // Declare a variable of type Rule for notational convenience
-// constexpr Rule rule{};
-// ```
-//
-// [heading Models]
-//
-// * __dec_octet_rule__
-// * __delim_rule__
-// * __not_empty_rule__
-// * __optional_rule__
-// * __range_rule__
-// * __token_rule__
-// * __tuple_rule__
-// * __unsigned_rule__
-// * __variant_rule__
-//
-// [endsect]
\ No newline at end of file
+[cols="a,a,a"]
+|===
+// Headers
+|Expression|Type|Semantics, Pre/Post-conditions
+
+// Row 1, Column 1
+|[source,cpp]
+----
+T(t)
+----
+
+// Row 1, Column 2
+| -
+// Row 1, Column 3
+|Copy construction of `T` throws nothing.
+
+`std::is_nothrow_copy_constructible::value == true`
+
+// Row 2, Column 1
+|[source,cpp]
+----
+T::value_type
+----
+
+// Row 2, Column 2
+| -
+// Row 2, Column 3
+|Values of this type are returned by the rule when the
+ parse operation is successful
+
+// Row 3, Column 1
+|[source,cpp]
+----
+t.parse(it,end)
+----
+
+// Row 3, Column 2
+|`result`
+// Row 3, Column 3
+|Attempt to parse the buffer of characters defined by
+the range `// [it,end)`. Upon success, the return result
+holds an instance of the rule's value type, and
+the reference parameter `it` is modified to point
+to the first unconsumed character. Otherwise, upon
+failure the result holds an error. In this case
+the implementation defines if and how the reference
+parameter `it` is modified.
+
+|===
+
+== Exemplar
+
+For best results, it is suggested that all constructors for
+rules be marked `constexpr`.
+
+[source,cpp]
+----
+struct Rule
+{
+ struct value_type;
+
+ constexpr Rule( Rule const& ) noexcept = default;
+
+ auto parse( char const*& it, char const* end ) const -> result< value_type >;
+};
+
+// Declare a variable of type Rule for notational convenience
+constexpr Rule rule{};
+----
+
+== Model
+
+* `grammar::dec_octet_rule`
+* `grammar::delim_rule`
+* `grammar::not_empty_rule`
+* `grammar::optional_rule`
+* `grammar::range_rule`
+* `grammar::token_rule`
+* `grammar::tuple_rule`
+* `grammar::unsigned_rule`
+* `grammar::variant_rule`
diff --git a/doc/modules/ROOT/pages/concepts/StringToken.adoc b/doc/modules/ROOT/pages/concepts/StringToken.adoc
index 027d3a93..0673b63f 100644
--- a/doc/modules/ROOT/pages/concepts/StringToken.adoc
+++ b/doc/modules/ROOT/pages/concepts/StringToken.adoc
@@ -8,7 +8,7 @@
//
-== StringToken
+= StringToken
A string token is an rvalue passed to a function template which
customizes the return type of the function and also controls how
@@ -18,7 +18,7 @@ function call in which it appears as a parameter. A string
token cannot be copied, moved, or assigned, and must be
destroyed when the function returns or throws.
-=== Requirements
+== Requirements
In this table:
@@ -26,73 +26,81 @@ In this table:
* `t` is an rvalue reference of type T
* `n` is a value of type `std::size_t`
-// [table Valid expressions
-// [[Expression] [Result] [Semantics, Pre/Post-conditions]]
-// [
-// [
-// ```
-// std::derived_from
-// ```
-// ]
-// [
-// ```
-// true
-// ```
-// ]
-// [
-// All string tokens must be publicly and
-// unambiguously derived from
-// [link url.ref.boost__urls__string_token__arg `string_token::arg`].
-// ]
-// ][
-// [
-// ```
-// T::result_type
-// ```
-// ]
-// []
-// [
-// This type determines the return type of functions
-// which accept a string token.
-// ]
-// ][
-// [
-// ```
-// t.prepare(n);
-// ```
-// ]
-// [
-// ```
-// char*
-// ```
-// ]
-// [
-// This function overrides the virtual function in the base.
-// It must return a pointer to a character buffer of at least
-// size `n`, otherwise throw an exception.
-// ]
-// ][
-// [
-// ```
-// t.result();
-// ```
-// ]
-// [
-// ```
-// T::result_type
-// ```
-// ]
-// [
-// This function is invoked by the algorithm to receive the result
-// from the string token.
-// It is only invoked if `prepare` returned successfuly and the
-// string token was not destroyed.
-// ]
-// ]]
+[cols="a,a,a"]
+|===
+// Headers
+|Expression|Result|Semantics, Pre/Post-conditions
+
+// Row 1, Column 1
+|[source,cpp]
+----
+std::derived_from
+----
+
+// Row 1, Column 2
+|[source,cpp]
+----
+true
+----
+
+// Row 1, Column 3
+|All string tokens must be publicly and
+unambiguously derived from
+`string_token::arg`.
+
+// Row 2, Column 1
+|[source,cpp]
+----
+T::result_type
+----
+
+// Row 2, Column 2
+|
+// Row 2, Column 3
+|This type determines the return type of functions
+which accept a string token.
+
+// Row 3, Column 1
+|[source,cpp]
+----
+t.prepare(n);
+----
+
+// Row 3, Column 2
+|[source,cpp]
+----
+char*
+----
+
+// Row 3, Column 3
+|This function overrides the virtual function in the base.
+It must return a pointer to a character buffer of at least
+size `n`, otherwise throw an exception.
+
+// Row 4, Column 1
+|[source,cpp]
+----
+t.result();
+----
+
+// Row 4, Column 3
+|[source,cpp]
+----
+T::result_type
+----
+
+// Row 4, Column 5
+|This function is invoked by the algorithm to receive the result
+from the string token.
+It is only invoked if `prepare` returned successfuly and the
+string token was not destroyed.
+
+|===
-=== Algorithm Requirements
+
+== Algorithm Requirements
When an algorithm accepts a string token, it must meet these requirements:
@@ -105,7 +113,7 @@ String tokens cannot be reused.
-=== Exemplars
+== Exemplars
String token prototype:
@@ -166,9 +174,9 @@ algorithm( StringToken&& token = {} ) ->
Models
-* `append_to`
-* `assign_to`
-* `preserve_size`
-* `append_to`
+* `string_token::return_string`
+* `string_token::assign_to`
+* `string_token::preserve_size`
+* `string_token::return_string`
diff --git a/doc/modules/ROOT/pages/concepts/index.adoc b/doc/modules/ROOT/pages/concepts/index.adoc
index 7fb3031e..fbfe8b67 100644
--- a/doc/modules/ROOT/pages/concepts/index.adoc
+++ b/doc/modules/ROOT/pages/concepts/index.adoc
@@ -8,12 +8,12 @@
//
-== Concepts
+= Concepts
This section describes all of the concepts defined by the library.
-// [include 5.1.CharSet.qbk]
-// [include 5.2.Rule.qbk]
-// [include 5.3.StringToken.qbk]
+
+
+
diff --git a/doc/modules/ROOT/pages/examples.adoc b/doc/modules/ROOT/pages/examples.adoc
deleted file mode 100644
index f91d0b41..00000000
--- a/doc/modules/ROOT/pages/examples.adoc
+++ /dev/null
@@ -1,144 +0,0 @@
-//
-// Copyright (c) 2023 Alan de Freitas (alandefreitas@gmail.com)
-//
-// Distributed under the Boost Software License, Version 1.0. (See accompanying
-// file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt)
-//
-// Official repository: https://github.com/boostorg/url
-//
-
-
-
-
-== Examples
-
-=== QR Code
-
-A QR code is a machine-readable two-dimensional barcode. They might contain data
-for a identifier or a URL to a website.
-
-This example shows how to construct and modify URLs to consume a third party API to
-generate QR Codes.
-
-[source,cpp]
-----
-// example_qrcode
-----
-
-
-
-=== Finicky
-
-This example shows how to classify URLs according to a set of rules. It is
-inspired by https://github.com/johnste/finicky[Finicky,window=blank_] application.
-
-The URLs are classified and redirected to a browser according to their
-category. See the example `config.json` file.
-
-[source,cpp]
-----
-// example_finicky
-----
-
-
-
-=== mailto URLs
-
-`mailto` is a URL scheme for email addresses. `mailto` URL are used on websites
-to allow users to send an email to a specific address directly from an HTML document.
-
-This example parses a mailto URL into a new view type and prints its components to
-standard output.
-
-[source,cpp]
-----
-// example_mailto
-----
-
-
-
-=== Magnet Link
-
-`magnet` is a URL scheme for identifying files by their content. These files are
-usually identified by cryptographic hash value.
-
-Magnet links are useful in peer-to-peer file sharing networks because they allow
-resources to be referred to without the need for a continuously available host..
-
-This example parses a magnet link into a new view type and prints its components to
-standard output.
-
-[source,cpp]
-----
-// example_magnet
-----
-
-
-
-=== File Router
-
-This example defines a router that associates URL paths to a directory in the filesystem. If
-the specified route matches and the file exists, the example prints its contents to standard output.
-
-[source,cpp]
-----
-// example_file_router
-----
-
-
-
-=== Router
-
-This example defines a router for URL paths. If the specified route matches one of the existing
-routes, the example executes the underlying callback function.
-
-[source,cpp]
-----
-// example_router
-----
-
-
-
-=== Sanitizing URLs
-
-This example parses a non-strict or invalid URL
-into path components according to its delimiters.
-This pattern can be adapted to the requirements of other
-applications.
-
-Once the non-strict components are determined, a new URL is
-created and its parts are set with the `set_encoded_X`
-functions, which will encode any invalid chars accordingly.
-
-This sort of transformation is useful in applications that are
-extremely loose in what kinds of URLs they accept, such as
-browsers. The sanitized URL can later be used for machine-to-machine
-communication.
-
-Using non-strict URLs directly is a security concern in
-machine-to-machine communication, is ambiguous, and also
-involve an extra cost for the transformations.
-
-Different transformations are required by different applications to
-construct a valid URL appropriate for machine-to-machine communication.
-For instance, if an invalid relative reference includes something that
-looks like a host in the first path segment, browsers usually interpret
-that as the host with an implicit "https" scheme. Other applications
-also have other implicit schemes.
-
-The example also identifies whether the input url is already valid.
-It includes diagnostics that can be used to help the user determine
-if a URL is invalid and why it's invalid.
-
-Once all transformations are applied, the result is a URL
-appropriate for machine-to-machine communication.
-
-[source,cpp]
-----
-// example_sanitize_url
-----
-
-
-
-
-
diff --git a/doc/modules/ROOT/pages/examples/file-router.adoc b/doc/modules/ROOT/pages/examples/file-router.adoc
new file mode 100644
index 00000000..d76075f9
--- /dev/null
+++ b/doc/modules/ROOT/pages/examples/file-router.adoc
@@ -0,0 +1,221 @@
+//
+// Copyright (c) 2023 Alan de Freitas (alandefreitas@gmail.com)
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt)
+//
+// Official repository: https://github.com/boostorg/url
+//
+
+
+
+
+= File Router
+
+This example defines a router that associates URL paths to a directory in the filesystem. If
+the specified route matches and the file exists, the example prints its contents to standard output.
+
+// example_file_router
+[source,cpp]
+----
+
+/*
+ This example defines a route for a URL path.
+ If the specified route matches and the file
+ exists, the example prints its contents to
+ standard output.
+*/
+
+
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace urls = boost::urls;
+namespace fs = boost::filesystem;
+namespace core = boost::core;
+using string_view = boost::core::string_view;
+
+/** Check if a target matches a prefix
+
+ This function checks if the first segments
+ of the target match the corresponding prefix
+ segments.
+
+ @param target Target segments
+ @param prefix Prefix segments
+ @return True if target matches prefix
+ */
+bool match_prefix(
+ urls::segments_view target,
+ urls::segments_view prefix)
+{
+ // Trivially reject target that cannot
+ // contain the prefix
+ if (target.size() < prefix.size())
+ return false;
+
+ // Match the prefix segments
+ auto it0 = target.begin();
+ auto end0 = target.end();
+ auto it1 = prefix.begin();
+ auto end1 = prefix.end();
+ while (
+ it0 != end0 &&
+ it1 != end1 &&
+ *it0 == *it1)
+ {
+ ++it0;
+ ++it1;
+ }
+ return it1 == end1;
+}
+
+/** A static route representing files in a directory
+
+ A route is a URL logical prefix representing
+ static files in the specified root directory.
+
+ The `match` function returns the corresponding
+ file for a given URL path.
+ */
+class route
+{
+public:
+ /// Constructor
+ route(core::string_view prefix, fs::path root)
+ : prefix_(urls::parse_uri_reference(prefix).value())
+ , root_(std::move(root))
+ {}
+
+ /// Constructor
+ route(urls::url prefix, fs::path root)
+ : prefix_(std::move(prefix))
+ , root_(std::move(root))
+ {}
+
+ /** Match target URL path with a file
+
+ This function attempts to match the target
+ URL path with the route prefix.
+
+ If the prefix matches, the target is
+ considered to represent a file in the root
+ directory. When that happens, the target
+ prefix is consumed and other segments are
+ appended to the root path.
+
+ The complete file path represented by the
+ target is returned as the output parameter
+ `result`.
+
+ @param target Target URL path
+ @param result An out-parameter holding the resulting path
+ @return `true` if target matches the directory
+ */
+ bool match(
+ urls::url_view target,
+ fs::path& result)
+ {
+ if (match_prefix(
+ target.segments(),
+ static_cast(prefix_).segments()))
+ {
+ result = root_;
+ auto segs = target.segments();
+ auto it = segs.begin();
+ auto end = segs.end();
+ std::advance(it, prefix_.segments().size());
+ while (it != end)
+ {
+ auto seg = *it;
+ result.append(seg.begin(), seg.end());
+ ++it;
+ }
+ return true;
+ }
+ return false;
+ }
+
+private:
+ urls::url prefix_;
+ fs::path root_;
+};
+
+int
+main(int argc, char **argv)
+{
+ namespace urls = boost::urls;
+ namespace fs = boost::filesystem;
+
+ // Check command line arguments.
+ if (argc != 4)
+ {
+ fs::path exec = argv[0];
+ exec = exec.filename();
+ std::cerr
+ << "Usage: " << exec
+ << " \n"
+ "target: path to make a request\n"
+ "prefix: url prefix\n"
+ "doc_root: dir to look for files\n";
+ return EXIT_FAILURE;
+ }
+
+ try {
+ urls::url target =
+ urls::parse_uri_reference(argv[1]).value();
+ target.normalize_path();
+
+ std::string prefix = argv[2];
+ fs::path root = argv[2];
+
+ if (!fs::is_directory(root))
+ {
+ std::cerr
+ << "Error: " << root
+ << " is not a directory\n";
+ return EXIT_FAILURE;
+ }
+
+ // Create route
+ route r(prefix, root);
+
+ // Check if target matches a file
+ fs::path result;
+ if (r.match(target, result))
+ {
+ fs::ifstream f(result);
+ std::string l;
+ while (std::getline(f, l))
+ std::cout << l << '\n';
+ f.close();
+ }
+ else
+ {
+ std::cout
+ << "No " << target << " in prefix "
+ << prefix << std::endl;
+ }
+ return EXIT_SUCCESS;
+ }
+ catch (std::exception &e)
+ {
+ std::cerr << e.what() << "\n";
+ return EXIT_FAILURE;
+ }
+}
+----
+
+
+
diff --git a/doc/modules/ROOT/pages/examples/finicky.adoc b/doc/modules/ROOT/pages/examples/finicky.adoc
new file mode 100644
index 00000000..cbd215d5
--- /dev/null
+++ b/doc/modules/ROOT/pages/examples/finicky.adoc
@@ -0,0 +1,413 @@
+//
+// Copyright (c) 2023 Alan de Freitas (alandefreitas@gmail.com)
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt)
+//
+// Official repository: https://github.com/boostorg/url
+//
+
+
+
+
+= Finicky
+
+This example shows how to classify URLs according to a set of rules. It is
+inspired by https://github.com/johnste/finicky[Finicky,window=blank_] application.
+
+The URLs are classified and redirected to a browser according to their
+category. See the example `config.json` file.
+
+// example_finicky
+[source,cpp]
+----
+
+/*
+ This example shows how to classify URLs
+ according to a set of rules. This example is
+ inspired by Finicky. The URLs are classified
+ and redirected to a browser according to their
+ category. See the example config.json file.
+ https://github.com/johnste/finicky
+*/
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace urls = boost::urls;
+namespace json = boost::json;
+namespace core = boost::core;
+
+json::value
+read_json( std::istream& is, json::error_code& ec )
+{
+ json::parse_options opt;
+ opt.allow_comments = true;
+ json::stream_parser p(json::storage_ptr(), opt);
+ std::string line;
+ while( std::getline( is, line ) )
+ {
+ p.write( line, ec );
+ if( ec )
+ return nullptr;
+ }
+ p.finish( ec );
+ if( ec )
+ return nullptr;
+ return p.release();
+}
+
+bool
+glob_match(
+ core::string_view pattern,
+ core::string_view str)
+{
+ // regex
+ if (str.starts_with("/") &&
+ str.ends_with("/"))
+ {
+ const boost::regex pr(pattern.begin() + 1, pattern.end() - 1);
+ return boost::regex_match(std::string(str), pr);
+ }
+
+ // literal
+ if (!pattern.contains('*'))
+ {
+ return pattern == str;
+ }
+
+ // glob
+ std::string p = pattern;
+ std::size_t i = p.find('*');
+ while (i != std::string::npos)
+ {
+ auto e = std::min(p.find_first_not_of('*', i), p.size());
+ std::size_t n = e - i;
+ if (n == 1)
+ {
+ p.replace(i, e, "[^/]*");
+ i += 5;
+ }
+ else
+ {
+ p.replace(i, e, ".*");
+ i += 2;
+ }
+ i = p.find('*', i);
+ }
+ const boost::regex pr(p);
+ return boost::regex_match(std::string(str), pr);
+}
+
+bool
+url_match(
+ json::value& mv,
+ urls::url const& u)
+{
+ if (mv.is_string())
+ {
+ json::string& p = mv.as_string();
+ return glob_match(u.buffer(), p);
+ }
+ else if (mv.is_array())
+ {
+ json::array& m = mv.as_array();
+ for (auto& mi: m)
+ {
+ if (!mi.is_string())
+ throw std::invalid_argument(
+ "handle match is not a string");
+ if (glob_match(mi.as_string(), u.buffer()))
+ return true;
+ }
+ }
+ else if (mv.is_object())
+ {
+ json::object& m = mv.as_object();
+ std::pair
+ field_values[] = {
+ {"protocol", u.scheme()},
+ {"authority", u.encoded_authority()},
+ {"username", u.encoded_user()},
+ {"user", u.encoded_user()},
+ {"password", u.encoded_password()},
+ {"userinfo", u.encoded_userinfo()},
+ {"host", u.encoded_host()},
+ {"port", u.port()},
+ {"path", u.encoded_path()},
+ {"pathname", u.encoded_path()},
+ {"query", u.encoded_query()},
+ {"search", u.encoded_query()},
+ {"fragment", u.encoded_fragment()},
+ {"hash", u.encoded_fragment()},
+ };
+ for (auto& p: field_values)
+ {
+ auto it = m.find(p.first);
+ if (it != m.end())
+ {
+ if (!it->value().is_string())
+ throw std::invalid_argument(
+ "match fields should be a strings");
+ if (glob_match(p.second, p.first))
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+#define CHECK(c, msg) \
+ if (!(c)) \
+ { \
+ std::cerr << msg << "\n"; \
+ return EXIT_FAILURE; \
+ }
+
+int main(int argc, char** argv)
+{
+ if (argc < 3) {
+ std::cout << argv[0] << "\n";
+ std::cout << "Usage: finicky \n"
+ "options:\n"
+ " : Configuration file\n"
+ " : The url to open\n"
+ "examples:\n"
+ " finicky config.json \"http://www.example.com\"\n";
+ return EXIT_FAILURE;
+ }
+
+ // Parse url
+ boost::system::result ru = urls::parse_uri(argv[2]);
+ CHECK(ru, "Invalid URL");
+ urls::url u = *ru;
+
+ // Open config file
+ std::fstream fin(argv[1]);
+ CHECK(fin.good(), "Cannot open configuration file");
+ json::error_code ec;
+ json::value c = read_json(fin, ec);
+ CHECK(!ec.failed(), "Cannot parse configuration file");
+ CHECK(c.is_object(), "Configuration file is not an object");
+ json::object& o = c.as_object();
+
+ // Set initial browser
+ auto bit = o.find("defaultBrowser");
+ CHECK(
+ bit != o.end(),
+ "Configuration file has no defaultBrowser");
+ CHECK(
+ bit->value().is_string(),
+ "defaultBrowser should be a string");
+ json::string& browser = bit->value().as_string();
+
+ // Apply rewrites to the input string
+ auto rsit = o.find("rewrite");
+ if (rsit != o.end())
+ {
+ CHECK(
+ rsit->value().is_array(),
+ "rewrite rules should be an array");
+ auto& rs = rsit->value().as_array();
+ for (auto& rv: rs)
+ {
+ CHECK(
+ rv.is_object(),
+ "individual rewrite rule should be an object");
+ json::object& r = rv.as_object();
+
+ // Look for match
+ auto mit = r.find("match");
+ CHECK(
+ mit != r.end(),
+ "rewrite rule should have a match field");
+ CHECK(
+ mit->value().is_object() || mit->value().is_string(),
+ "rewrite match field is not an object");
+ if (!url_match(mit->value(), u))
+ continue;
+
+ // Apply replacement rule
+ auto uit = r.find("url");
+ CHECK(
+ uit != r.end(),
+ "rewrite rule should have a url field");
+ CHECK(
+ uit->value().is_object() ||
+ uit->value().is_string(),
+ "url field must be an object or string");
+
+ if (uit->value().is_string())
+ {
+ json::string& uo = uit->value().as_string();
+ auto ru1 = urls::parse_uri(uo);
+ CHECK(ru1, "url " << uo.c_str() << " is invalid");
+ u = *ru;
+ }
+ else
+ {
+ json::object& uo = uit->value().as_object();
+ auto it = uo.find("protocol");
+ if (it != uo.end())
+ {
+ CHECK(
+ it->value().is_string(),
+ "protocol field should be a string");
+ u.set_scheme(it->value().as_string());
+ }
+
+ it = uo.find("authority");
+ if (it != uo.end())
+ {
+ CHECK(
+ it->value().is_string(),
+ "authority field should be a string");
+ u.set_encoded_authority(
+ it->value().as_string().subview());
+ }
+
+ it = uo.find("username");
+ if (it == uo.end())
+ it = uo.find("user");
+ if (it != uo.end())
+ {
+ CHECK(
+ it->value().is_string(),
+ "username field should be a string");
+ u.set_encoded_user(
+ it->value().as_string().subview());
+ }
+
+ it = uo.find("password");
+ if (it != uo.end())
+ {
+ CHECK(
+ it->value().is_string(),
+ "password field should be a string");
+ u.set_encoded_password(
+ it->value().as_string().subview());
+ }
+
+ it = uo.find("userinfo");
+ if (it != uo.end())
+ {
+ CHECK(
+ it->value().is_string(),
+ "userinfo field should be a string");
+ u.set_encoded_userinfo(
+ it->value().as_string().subview());
+ }
+
+ it = uo.find("host");
+ if (it != uo.end())
+ {
+ CHECK(
+ it->value().is_string(),
+ "host field should be a string");
+ u.set_encoded_host(
+ it->value().as_string().subview());
+ }
+
+ it = uo.find("port");
+ if (it != uo.end())
+ {
+ CHECK(
+ it->value().is_string(),
+ "port field should be a string");
+ u.set_port(
+ it->value().as_string().subview());
+ }
+
+ it = uo.find("path");
+ if (it == uo.end())
+ it = uo.find("pathname");
+ if (it != uo.end())
+ {
+ CHECK(
+ it->value().is_string(),
+ "path field should be a string");
+ u.set_encoded_path(
+ it->value().as_string().subview());
+ }
+
+ it = uo.find("query");
+ if (it == uo.end())
+ it = uo.find("search");
+ if (it != uo.end())
+ {
+ CHECK(
+ it->value().is_string(),
+ "query field should be a string");
+ u.set_encoded_query(
+ it->value().as_string().subview());
+ }
+
+ it = uo.find("fragment");
+ if (it == uo.end())
+ it = uo.find("hash");
+ if (it != uo.end())
+ {
+ CHECK(
+ it->value().is_string(),
+ "fragment field should be a string");
+ u.set_encoded_fragment(
+ it->value().as_string().subview());
+ }
+ }
+ }
+ }
+
+ // Determine which browser should handle the url
+ auto hsit = o.find("handlers");
+ if (hsit != o.end())
+ {
+ CHECK(
+ hsit->value().is_array(),
+ "handler rules should be an array");
+ auto& hs = hsit->value().as_array();
+ for (auto& hv: hs)
+ {
+ CHECK(
+ hv.is_object(),
+ "individual handlers should be an object");
+ json::object& h = hv.as_object();
+
+ auto mit = h.find("match");
+ CHECK(
+ mit != h.end(),
+ "handle rule should have a match field");
+ CHECK(
+ mit->value().is_string() || mit->value().is_array(),
+ "handle match field must be an array or a string");
+
+ auto hbit = h.find("browser");
+ CHECK(
+ hbit != h.end(),
+ "handle rule should have a browser field");
+ CHECK(
+ hbit->value().is_string(),
+ "browser field is not a string");
+
+ // Look for match and change browser
+ if (url_match(mit->value(), u))
+ {
+ browser = hbit->value().as_string().subview();
+ break;
+ }
+ }
+ }
+
+ // Print command finicky would run
+ std::cout << "\"" << browser.c_str() << "\" " << u << '\n';
+
+ return EXIT_SUCCESS;
+}
+----
+
+
diff --git a/doc/modules/ROOT/pages/examples/index.adoc b/doc/modules/ROOT/pages/examples/index.adoc
new file mode 100644
index 00000000..22446787
--- /dev/null
+++ b/doc/modules/ROOT/pages/examples/index.adoc
@@ -0,0 +1,24 @@
+//
+// Copyright (c) 2023 Alan de Freitas (alandefreitas@gmail.com)
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt)
+//
+// Official repository: https://github.com/boostorg/url
+//
+
+
+
+
+= Examples
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/modules/ROOT/pages/examples/magnet-link.adoc b/doc/modules/ROOT/pages/examples/magnet-link.adoc
new file mode 100644
index 00000000..613046e1
--- /dev/null
+++ b/doc/modules/ROOT/pages/examples/magnet-link.adoc
@@ -0,0 +1,733 @@
+//
+// Copyright (c) 2023 Alan de Freitas (alandefreitas@gmail.com)
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt)
+//
+// Official repository: https://github.com/boostorg/url
+//
+
+
+
+
+= Magnet Link
+
+`magnet` is a URL scheme for identifying files by their content. These files are
+usually identified by cryptographic hash value.
+
+Magnet links are useful in peer-to-peer file sharing networks because they allow
+resources to be referred to without the need for a continuously available host..
+
+This example parses a magnet link into a new view type and prints its components to
+standard output.
+
+// example_magnet
+[source,cpp]
+----
+
+/*
+ This example parses a magnet link into a new
+ view type and prints its components to
+ standard output.
+*/
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "filter_view.hpp"
+#include
+
+namespace urls = boost::urls;
+namespace core = boost::core;
+
+/** Callable to identify a magnet "exact topic"
+
+ This callable evaluates if a query parameter
+ represents a magnet "exact topic".
+
+ This callable is used as a filter for
+ the topics_view.
+ */
+struct is_exact_topic
+{
+ bool
+ operator()(urls::param_view p);
+};
+
+/** Callable to identify a magnet url parameter
+
+ This callable evaluates if a query parameter
+ has a given key and a url as its value.
+
+ These urls are percent-encoded twice,
+ which means we need to decode it once
+ before attempting to parse it.
+
+ This callable is used as a filter for
+ the keys_view.
+ */
+class is_url_with_key
+{
+ core::string_view k_;
+public:
+ is_url_with_key(
+ core::string_view key)
+ : k_(key) {}
+
+ bool
+ operator()(urls::param_view p);
+};
+
+/** Callable to convert param values to urls
+
+ This callable converts the value of a
+ query parameter into a urls::url_view.
+
+ This callable is used as a transform
+ function for the topics_view.
+ */
+struct param_view_to_url
+{
+ urls::url
+ operator()(urls::param_view p);
+};
+
+/** Callable to convert param values to std::string
+
+ This callable converts the value of a
+ query parameter into a std::string.
+
+ This callable is used as a transform
+ function for the keys_view.
+ */
+struct to_decoded_value
+{
+ std::string
+ operator()(urls::param_view p)
+ {
+ return p.value;
+ }
+};
+
+/** Callable to convert param values to info_hashes
+
+ This callable converts the value of a
+ query parameter into a core::string_view with
+ its infohash.
+
+ The infohash hash is a parameter of an
+ exact topic field in the magnet link.
+
+ This callable is used as a transform
+ function for the info_hashes_view.
+ */
+struct param_view_to_infohash
+{
+ core::string_view
+ operator()(urls::param_view p);
+};
+
+/** Callable to convert param values to protocols
+
+ This callable converts the value of a
+ query parameter into a core::string_view with
+ its protocol.
+
+ The protocol is a parameter of an exact
+ topic field in the magnet link.
+
+ This callable is used as a transform
+ function for the protocols_view.
+ */
+struct to_protocol
+{
+ core::string_view
+ operator()(urls::param_view p);
+};
+
+struct magnet_link_rule_t;
+
+/** A new url type for magnet links
+
+ This class represents a reference to a
+ magnet link.
+
+ Unlike a urls::url_view, which only represents the
+ general syntax of urls, a magnet_link_view
+ represents a reference to fields that are
+ relevant to magnet links, while ignoring
+ elements of the general syntax
+ that are not relevant to the scheme.
+
+ This allows us to use the general syntax
+ parsers to create a representation that
+ is more appropriate for the specified scheme
+ syntax.
+
+ @par Specification
+ @li DHT Protocol
+ @li Extension for Peers to Send Metadata Files
+ @li Magnet URI extension
+ @li Magnet URI scheme
+
+ @par References
+ @li magnet-uri
+
+ */
+class magnet_link_view
+{
+ urls::url_view u_;
+
+public:
+ /// A view of all exact topics in the magnet_link
+ using topics_view =
+ filter_view<
+ urls::params_view,
+ urls::url,
+ is_exact_topic,
+ param_view_to_url>;
+
+ /// A view of all info_hashes in the magnet_link
+ using info_hashes_view =
+ filter_view<
+ urls::params_view,
+ std::string,
+ is_exact_topic,
+ param_view_to_infohash>;
+
+ /// A view of all protocols in the magnet_link
+ using protocols_view =
+ filter_view<
+ urls::params_view,
+ std::string,
+ is_exact_topic,
+ to_protocol>;
+
+ /** A view of all urls with the specified key in the magnet_link
+
+ A number of fields in a magnet link refer
+ to a list of urls with the same query
+ parameter keys.
+ */
+ using keys_view =
+ filter_view<
+ urls::params_view,
+ std::string,
+ is_url_with_key,
+ to_decoded_value>;
+
+ /** URNs to the file or files hashes
+
+ An exact topic is the main field of a
+ magnet link. A magnet link must contain
+ one or more exact topics with the query
+ key "xt" or ["xt.1", "xt.2", ...].
+
+ The value of each exact topic is a URN
+ representing the file hash and the protocol
+ to access the file.
+
+ @return A view of all exact topic URNs in the link
+ */
+ topics_view
+ exact_topics() const noexcept;
+
+ /** Info hash of the file or files
+
+ @return A view of all info hashes in exact topics
+ */
+ info_hashes_view
+ info_hashes() const noexcept;
+
+ /** Protocol of the exact topics
+
+ @return A view of all protocols in exact topics
+ */
+ protocols_view
+ protocols() const noexcept;
+
+ /** Return view of address trackers
+
+ A tracker URL is used to obtain resources
+ for BitTorrent downloads.
+
+ @return A view of all address trackers in the link
+ */
+ keys_view
+ address_trackers() const;
+
+ /** Return view of exact sources
+
+ An exact source URL is a direct download
+ link to the file.
+
+ @return A view of all exact sources
+ */
+ keys_view
+ exact_sources() const;
+
+ /** Return view of acceptable sources
+
+ An acceptable source URL is a direct
+ download link to the file that can be
+ used as a fallback for exact sources.
+
+ @return A view of all acceptable sources
+ */
+ keys_view
+ acceptable_sources() const;
+
+ /** Return keyword topic
+
+ The keyword topic is the search keywords
+ to use in P2P networks.
+
+ @par Example
+ kt=martin+luther+king+mp3
+
+ @return Keyword topic
+ */
+ boost::optional
+ keyword_topic() const noexcept;
+
+ /** Return manifest topics
+
+ This function returns a link to the
+ metafile that contains a list of magneto.
+
+ @par Specification
+ @li MAGnet MAnifest
+
+ @return A view of manifest topics
+ */
+ keys_view
+ manifest_topics() const;
+
+ /** Return display name
+
+ This function returns a filename to
+ display to the user. This field is
+ only used for convenience.
+
+ @par Specification
+ @li MAGnet MAnifest
+
+ @return Display name
+ */
+ boost::optional
+ display_name() const noexcept;
+
+ /** Return web seed
+
+ The web seed represents the payload data
+ served over HTTP(S).
+
+ @return Web seed
+ */
+ keys_view
+ web_seed() const;
+
+ /** Return extra supplement parameter
+
+ This function returns informal options
+ and parameters of the magnet link.
+
+ Query parameters whose keys have the
+ prefix "x." are used in magnet links
+ for extra parameters. These names
+ are guaranteed to never be standardized.
+
+ @par Example
+ x.parameter_name=parameter_data
+
+ @return Web seed
+ */
+ boost::optional
+ param(core::string_view key) const noexcept;
+
+ friend
+ std::ostream&
+ operator<<(std::ostream& os, magnet_link_view m)
+ {
+ return os << m.u_;
+ }
+
+private:
+ // get a query parameter as a urls::pct_string_view
+ boost::optional
+ encoded_param(core::string_view key) const noexcept;
+
+ // get a query parameter as a urls::url_view
+ boost::optional
+ url_param(core::string_view key) const noexcept;
+
+ friend magnet_link_rule_t;
+};
+
+bool
+is_exact_topic::
+operator()(urls::param_view p)
+{
+ // These comparisons use the lazy
+ // operator== for urls::pct_string_view
+ // For instance, the comparison also works
+ // if the underlying key is "%78%74"/
+ if (p.key == "xt")
+ return true;
+ return
+ p.key.size() > 3 &&
+ *std::next(p.key.begin(), 0) == 'x' &&
+ *std::next(p.key.begin(), 1) == 't' &&
+ *std::next(p.key.begin(), 2) == '.' &&
+ std::all_of(
+ std::next(p.key.begin(), 3),
+ p.key.end(),
+ urls::grammar::digit_chars);
+}
+
+bool
+is_url_with_key::
+operator()(urls::param_view p)
+{
+ if (p.key != k_)
+ return false;
+ boost::system::error_code ec;
+ std::string buf(
+ p.value.begin(), p.value.end());
+ if (ec.failed())
+ return false;
+ boost::system::result r =
+ urls::parse_uri(buf);
+ return r.has_value();
+}
+
+urls::url
+param_view_to_url::
+operator()(urls::param_view p)
+{
+ // `param_view_to_url` is used in topics_view,
+ // where the URL is not
+ // percent-encoded twice.
+ // Thus, we can already parse the
+ // encoded value.
+ auto ur =
+ urls::parse_uri(p.value);
+ BOOST_ASSERT(ur);
+ urls::url u = *ur;
+ return u;
+}
+
+core::string_view
+param_view_to_infohash::
+operator()(urls::param_view p)
+{
+ urls::url_view topic =
+ urls::parse_uri(p.value).value();
+ core::string_view t = topic.encoded_path();
+ std::size_t pos = t.find_last_of(':');
+ if (pos != core::string_view::npos)
+ return t.substr(pos + 1);
+ return t;
+}
+
+core::string_view
+to_protocol::
+operator()(urls::param_view p)
+{
+ urls::url_view topic =
+ urls::parse_uri(p.value).value();
+ core::string_view t = topic.encoded_path();
+ std::size_t pos = t.find_last_of(':');
+ return t.substr(0, pos);
+}
+
+auto
+magnet_link_view::exact_topics() const noexcept
+ -> topics_view
+{
+ return {u_.params()};
+}
+
+auto
+magnet_link_view::info_hashes() const noexcept
+ -> info_hashes_view
+{
+ return {u_.params()};
+}
+
+auto
+magnet_link_view::protocols() const noexcept
+ -> protocols_view
+{
+ return {u_.params()};
+}
+
+auto
+magnet_link_view::address_trackers() const
+ -> keys_view
+{
+ return {
+ u_.params(),
+ is_url_with_key{"tr"}};
+}
+
+auto
+magnet_link_view::exact_sources() const
+ -> keys_view
+{
+ return {
+ u_.params(),
+ is_url_with_key{"xs"}};
+}
+
+auto
+magnet_link_view::acceptable_sources() const
+ -> keys_view
+{
+ return {
+ u_.params(),
+ is_url_with_key{"as"}};
+}
+
+boost::optional
+magnet_link_view::keyword_topic() const noexcept
+{
+ boost::optional o =
+ encoded_param("kt");
+ if (o)
+ return o->decode();
+ return boost::none;
+}
+
+auto
+magnet_link_view::manifest_topics() const
+ -> keys_view
+{
+ return {
+ u_.params(),
+ is_url_with_key{"mt"}};
+}
+
+boost::optional
+magnet_link_view::display_name() const noexcept
+{
+ return encoded_param("dn");
+}
+
+auto
+magnet_link_view::web_seed() const
+ -> keys_view
+{
+ return {
+ u_.params(),
+ is_url_with_key{"ws"}};
+}
+
+boost::optional
+magnet_link_view::param(core::string_view key) const noexcept
+{
+ urls::params_view ps = u_.params();
+ auto it = ps.begin();
+ auto end = ps.end();
+ while (it != end)
+ {
+ urls::param_view p = *it;
+ if (p.key.size() < 2)
+ {
+ ++it;
+ continue;
+ }
+ auto first = p.key.begin();
+ auto mid = std::next(p.key.begin(), 2);
+ auto last = p.key.end();
+ urls::pct_string_view prefix(
+ core::string_view(first, mid));
+ urls::pct_string_view suffix(
+ core::string_view(mid, last));
+ if (prefix == "x." &&
+ suffix == key &&
+ p.has_value)
+ return urls::pct_string_view(p.value);
+ ++it;
+ }
+ return boost::none;
+}
+
+boost::optional
+magnet_link_view::encoded_param(core::string_view key) const noexcept
+{
+ urls::params_encoded_view ps = u_.encoded_params();
+ auto it = ps.find(key);
+ if (it != ps.end() && (*it).has_value)
+ return urls::pct_string_view((*it).value);
+ return boost::none;
+}
+
+boost::optional
+magnet_link_view::url_param(core::string_view key) const noexcept
+{
+ urls::params_encoded_view ps = u_.encoded_params();
+ auto it = ps.find(key);
+ if (it != ps.end() && (*it).has_value)
+ {
+ boost::system::result r =
+ urls::parse_uri((*it).value);
+ if (r)
+ return *r;
+ }
+ return boost::none;
+}
+
+/** Rule to match a magnet link
+*/
+struct magnet_link_rule_t
+{
+ /// Value type returned by the rule
+ using value_type = magnet_link_view;
+
+ /// Parse a sequence of characters into a magnet_link_view
+ boost::system::result< value_type >
+ parse( char const*& it, char const* end ) const noexcept;
+};
+
+auto
+magnet_link_rule_t::parse(
+ char const*& it,
+ char const* end ) const noexcept
+ -> boost::system::result< value_type >
+{
+ // 1) Parse url with the general uri syntax
+ boost::system::result r =
+ urls::grammar::parse(it, end, urls::absolute_uri_rule);
+ if(!r)
+ return urls::grammar::error::invalid;
+ magnet_link_view m;
+ m.u_ = *r;
+
+ // 2) Check if exact topics are valid urls
+ // and that we have at least one. This is the
+ // only mandatory field in magnet links.
+ auto ps = m.u_.params();
+ auto pit = ps.begin();
+ auto pend = ps.end();
+ pit = std::find_if(pit, pend, is_exact_topic{});
+ if (pit == pend)
+ {
+ // no exact topic in the magnet link
+ return urls::grammar::error::invalid;
+ }
+
+ // all topics should parse as valid urls
+ if (!std::all_of(pit, pend, [](
+ urls::param_view p)
+ {
+ if (!is_exact_topic{}(p))
+ return true;
+ boost::system::result u =
+ urls::parse_uri(p.value);
+ return u.has_value();
+ }))
+ return urls::grammar::error::invalid;
+
+ // all other fields are optional
+ // magnet link is OK
+ return m;
+}
+
+constexpr magnet_link_rule_t magnet_link_rule{};
+
+/** Return a parsed magnet link from a string, or error.
+
+ This is a more convenient user-facing function
+ to parse magnet links.
+*/
+boost::system::result< magnet_link_view >
+parse_magnet_link( core::string_view s ) noexcept
+{
+ return urls::grammar::parse(s, magnet_link_rule);
+}
+
+int main(int argc, char** argv)
+{
+ // This example shows how to use custom parsing
+ // to process alternate URI schemes, in this
+ // case "magnet"
+ if (argc != 2) {
+ std::cout << argv[0] << "\n";
+ std::cout << "magnet \n"
+ "example: magnet magnet:?xt=urn:btih:d2474e86c95b19b8bcfdb92bc12c9d44667cfa36"
+ "&dn=Leaves+of+Grass+by+Walt+Whitman.epub"
+ "&tr=udp%3A%2F%2Ftracker.example4.com%3A80"
+ "&tr=udp%3A%2F%2Ftracker.example5.com%3A80"
+ "&tr=udp%3A%2F%2Ftracker.example3.com%3A6969"
+ "&tr=udp%3A%2F%2Ftracker.example2.com%3A80"
+ "&tr=udp%3A%2F%2Ftracker.example1.com%3A1337\n";
+ return EXIT_FAILURE;
+ }
+
+ boost::system::result r =
+ parse_magnet_link(argv[1]);
+ if (!r)
+ return EXIT_FAILURE;
+
+ magnet_link_view m = *r;
+ std::cout << "link: " << m << "\n";
+
+ auto xt = m.exact_topics();
+ for (auto h : xt)
+ std::cout << "topic: " << h << "\n";
+
+ auto hs = m.info_hashes();
+ for (auto h : hs)
+ std::cout << "hash: " << h << "\n";
+
+ auto ps = m.protocols();
+ for (auto p : ps)
+ std::cout << "protocol: " << p << "\n";
+
+ auto tr = m.address_trackers();
+ for (auto h : tr)
+ std::cout << "tracker: " << h << "\n";
+
+ auto xs = m.exact_sources();
+ for (auto x : xs)
+ std::cout << "exact source: " << x << "\n";
+
+ auto as = m.acceptable_sources();
+ for (auto a : as)
+ std::cout << "topic: " << a << "\n";
+
+ auto mt = m.manifest_topics();
+ for (auto a : mt)
+ std::cout << "manifest topic: " << a << "\n";
+
+ auto ws = m.web_seed();
+ for (auto a : ws)
+ std::cout << "web seed: " << a << "\n";
+
+ auto kt = m.keyword_topic();
+ if (kt)
+ std::cout << "keyword topic: " << *kt << "\n";
+
+ auto dn = m.display_name();
+ if (dn)
+ std::cout << "display name: " << *dn << "\n";
+
+ return EXIT_SUCCESS;
+}
+----
+
+
diff --git a/doc/modules/ROOT/pages/examples/mailto.adoc b/doc/modules/ROOT/pages/examples/mailto.adoc
new file mode 100644
index 00000000..016fb44c
--- /dev/null
+++ b/doc/modules/ROOT/pages/examples/mailto.adoc
@@ -0,0 +1,428 @@
+//
+// Copyright (c) 2023 Alan de Freitas (alandefreitas@gmail.com)
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt)
+//
+// Official repository: https://github.com/boostorg/url
+//
+
+
+
+
+= mailto URLs
+
+`mailto` is a URL scheme for email addresses. `mailto` URL are used on websites
+to allow users to send an email to a specific address directly from an HTML document.
+
+This example parses a mailto URL into a new view type and prints its components to
+standard output.
+
+// example_mailto
+[source,cpp]
+----
+
+/*
+ This example parses a mailto URL into a new
+ view type and prints its components to
+ standard output.
+*/
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "rfc.hpp"
+
+namespace urls = boost::urls;
+
+// fwd-declaration for mailto_view
+struct mailto_rule_t;
+
+/// A new url type for mailto URLs
+/**
+ This class represents a URI with the mailto
+ scheme.
+
+ Unlike a urls::url_view, which only represents
+ the general syntax of urls, a mailto_view
+ represents a reference to fields that are
+ relevant to mailto URLs, while ignoring
+ elements of the general syntax
+ that are not relevant to the scheme.
+
+ This allows us to use the general syntax
+ parsers to create a representation that
+ is more appropriate for the specified scheme
+ syntax.
+
+ @par Specification
+ @li The 'mailto' URI Scheme
+ @li RFC Errata Report
+
+ @par References
+ @li mailto (Wikipedia)
+
+ */
+class mailto_view
+{
+ urls::url_view u_;
+
+public:
+ /// Return the specified email address in the URL
+ /**
+ A mailto URL might contain multiple email
+ addresses separated by commas.
+
+ The first addresses are represented in
+ the path. Other addresses are in
+ any query parameter whose key is "to".
+
+ @param i Address index
+
+ @return The specified address
+ */
+ std::string
+ address(std::size_t i = 0) const;
+
+ /// @copydoc address()
+ urls::pct_string_view
+ encoded_address(std::size_t i = 0) const noexcept;
+
+ /// Return number of email addresses in the URL
+ std::size_t
+ size() const noexcept;
+
+ /// Return the specified cc email address in the URL
+ /**
+ A mailto URL might contain multiple cc
+ email addresses separated by commas.
+
+ Addresses can be represented in any query
+ parameter whose key is "cc".
+
+ @param i Address index
+
+ @return The specified cc address
+ */
+ std::string
+ cc(std::size_t i) const;
+
+ /// @copydoc cc()
+ urls::pct_string_view
+ encoded_cc(std::size_t i) const noexcept;
+
+ /// Return number of "cc" email addresses in the URL
+ std::size_t
+ size_cc() const noexcept;
+
+ /// Return email message subject
+ std::string
+ subject() const;
+
+ /// @copydoc subject()
+ urls::pct_string_view
+ encoded_subject() const noexcept;
+
+ /// Return email message body
+ std::string
+ body() const;
+
+ /// @copydoc body()
+ urls::pct_string_view
+ encoded_body() const noexcept;
+
+ friend
+ std::ostream&
+ operator<<(std::ostream& os, mailto_view m)
+ {
+ return os << m.u_;
+ }
+
+private:
+ // Count number of addresses in a string
+ static
+ std::size_t
+ addr_in_str(boost::core::string_view s);
+
+ // Get the ith address from a string
+ static
+ boost::optional
+ get_nth_address(boost::core::string_view to, std::size_t &i) noexcept;
+
+ // Get param value or empty otherwise
+ urls::pct_string_view
+ param_or_empty(urls::pct_string_view k) const noexcept;
+
+ friend mailto_rule_t;
+};
+
+/** Rule to match a mailto URL
+*/
+struct mailto_rule_t
+{
+ /// Value type returned by the rule
+ using value_type = mailto_view;
+
+ /// Parse a sequence of characters into a mailto_view
+ boost::system::result< value_type >
+ parse( char const*& it, char const* end ) const noexcept;
+};
+
+constexpr mailto_rule_t mailto_rule{};
+
+/** Return a parsed mailto URL from a string, or error.
+
+ This is a more convenient user-facing function
+ to parse mailto URLs.
+*/
+boost::system::result< mailto_view >
+parse_mailto( boost::core::string_view s ) noexcept
+{
+ return urls::grammar::parse(s, mailto_rule);
+}
+
+int main(int argc, char** argv)
+{
+ // This example shows how to use custom parsing
+ // to process alternate URI schemes, in this
+ // case "mailto"
+ if (argc != 2) {
+ std::cout << argv[0] << "\n";
+ std::cout << "mailto \n"
+ "examples:\n"
+ // Single e-mail address
+ "mailto mailto:someone@example.com\n"
+ // Two e-mail addresses
+ "mailto mailto:someone@example.com,someoneelse@example.com\n"
+ // E-mail headers
+ "mailto mailto:someone@example.com?subject=Our%20meeting&cc=someone_else@example.com&body=Hi%21\n"
+ // E-mail headers only
+ "mailto mailto:?to=&subject=mailto%20example&body=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FMailto\n"
+ // All fields
+ "mailto mailto:someone@example.com,%73omeoneelse@me.com?to=thirdperson@example.com&subject=Our%20meeting&cc=someone_else@example.com,onemore@ex%61mple.com&body=Hi%21\n";
+ return EXIT_FAILURE;
+ }
+
+ boost::system::result r =
+ parse_mailto(argv[1]);
+ if (!r)
+ return EXIT_FAILURE;
+
+ mailto_view m = *r;
+ std::cout << "link: " << m << "\n";
+
+ for (std::size_t i = 0; i < m.size(); ++i)
+ std::cout <<
+ "to[" << i << "]: " <<
+ m.address(i) << "\n";
+
+ for (std::size_t i = 0; i < m.size_cc(); ++i)
+ std::cout <<
+ "cc[" << i << "]: " <<
+ m.address(i) << "\n";
+
+ std::cout << "subject: " << m.subject() << "\n";
+ std::cout << "body: " << m.body() << "\n";
+
+ return EXIT_SUCCESS;
+}
+
+std::string
+mailto_view::address(std::size_t i) const
+{
+ return encoded_address(i).decode();
+}
+
+urls::pct_string_view
+mailto_view::encoded_address(std::size_t i) const noexcept
+{
+ // Look for ith email address in the path string
+ auto s = get_nth_address(u_.encoded_path(), i);
+ if (s)
+ return *s;
+
+ // Look for ith email address in one of the "to" headers
+ auto ps = u_.encoded_params();
+ auto it = ps.find("to", urls::ignore_case);
+ while (it != ps.end())
+ {
+ s = get_nth_address((*it++).value, i);
+ if (s)
+ return *s;
+ it = ps.find(it, "to", urls::ignore_case);
+ }
+ return {};
+}
+
+std::size_t
+mailto_view::size() const noexcept
+{
+ // Count addresses in path
+ std::size_t n = addr_in_str(u_.encoded_path());
+
+ // Count addresses in "to" headers
+ auto ps = u_.encoded_params();
+ auto it = ps.find("to", urls::ignore_case);
+ while (it != ps.end())
+ {
+ n += addr_in_str((*it++).value);
+ it = ps.find(it, "to", urls::ignore_case);
+ }
+ return n;
+}
+
+std::string
+mailto_view::cc(std::size_t i) const
+{
+ return encoded_cc(i).decode();
+}
+
+urls::pct_string_view
+mailto_view::encoded_cc(std::size_t i) const noexcept
+{
+ // Look for ith email address in one of the "to" headers
+ auto ps = u_.encoded_params();
+ auto it = ps.find("cc", urls::ignore_case);
+ while (it != ps.end())
+ {
+ auto s = get_nth_address((*it++).value, i);
+ if (s)
+ return *s;
+ it = ps.find(it, "cc", urls::ignore_case);
+ }
+ return {};
+}
+
+std::size_t
+mailto_view::size_cc() const noexcept
+{
+ // Count addresses in "to" headers
+ std::size_t n = 0;
+ auto ps = u_.encoded_params();
+ auto it = ps.find("cc", urls::ignore_case);
+ while (it != ps.end())
+ {
+ n += addr_in_str((*it++).value);
+ it = ps.find(it, "cc", urls::ignore_case);
+ }
+ return n;
+}
+
+std::string
+mailto_view::subject() const
+{
+ return encoded_subject().decode();
+}
+
+urls::pct_string_view
+mailto_view::encoded_subject() const noexcept
+{
+ return param_or_empty("subject");
+}
+
+std::string
+mailto_view::mailto_view::body() const
+{
+ return encoded_body().decode();
+}
+
+urls::pct_string_view
+mailto_view::encoded_body() const noexcept
+{
+ return param_or_empty("body");
+}
+
+std::size_t
+mailto_view::addr_in_str(boost::core::string_view s)
+{
+ std::size_t n = 0;
+ bool empty = true;
+ for (char c : s)
+ {
+ if (c == ',')
+ {
+ n += !empty;
+ empty = true;
+ }
+ else
+ {
+ empty = false;
+ }
+ }
+ n += !empty;
+ return n;
+}
+
+boost::optional
+mailto_view::get_nth_address(boost::core::string_view to, std::size_t &i) noexcept
+{
+ auto p = to.find(',');
+ while (p != boost::core::string_view::npos)
+ {
+ if (i == 0)
+ return urls::pct_string_view(
+ to.substr(0, p));
+ --i;
+ to.remove_prefix(p + 1);
+ p = to.find(',');
+ }
+ if (!to.empty())
+ {
+ if (i == 0)
+ return urls::pct_string_view(
+ to.substr(0, p));
+ --i;
+ }
+ return boost::none;
+}
+
+urls::pct_string_view
+mailto_view::param_or_empty(urls::pct_string_view k) const noexcept
+{
+ auto ps = u_.encoded_params();
+ auto it = ps.find(k, urls::ignore_case);
+ if (it != ps.end())
+ return (*it).value;
+ return {};
+}
+
+auto
+mailto_rule_t::parse( char const*& it, char const* end ) const noexcept
+ -> boost::system::result< value_type >
+{
+ // Syntax-based rules
+ boost::system::result r =
+ urls::grammar::parse(it, end, urls::absolute_uri_rule);
+ if (!r)
+ return r.error();
+
+ // Scheme-based rules
+ mailto_view m;
+ m.u_ = *r;
+ auto valid_header = [](urls::param_pct_view p) {
+ return
+ urls::grammar::parse(p.key, hfname_rule) &&
+ urls::grammar::parse(p.value, hfvalue_rule) &&
+ p.has_value &&
+ (!urls::grammar::ci_is_equal(p.key, "to") ||
+ urls::grammar::parse(p.value, addr_spec_rule));
+ };
+ auto ps = m.u_.encoded_params();
+ if (m.u_.scheme() == "mailto" &&
+ !m.u_.has_authority() &&
+ urls::grammar::parse(m.u_.encoded_path(), to_rule) &&
+ std::all_of(ps.begin(), ps.end(), valid_header))
+ return m;
+ return urls::grammar::error::invalid;
+}
+----
+
+
diff --git a/doc/modules/ROOT/pages/examples/qrcode.adoc b/doc/modules/ROOT/pages/examples/qrcode.adoc
new file mode 100644
index 00000000..5668654a
--- /dev/null
+++ b/doc/modules/ROOT/pages/examples/qrcode.adoc
@@ -0,0 +1,105 @@
+//
+// Copyright (c) 2023 Alan de Freitas (alandefreitas@gmail.com)
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt)
+//
+// Official repository: https://github.com/boostorg/url
+//
+
+
+
+
+= QR Code
+
+A QR code is a machine-readable two-dimensional barcode. They might contain data
+for a identifier or a URL to a website.
+
+This example shows how to construct and modify URLs to consume a third party API to
+generate QR Codes.
+
+// example_qrcode
+[source,cpp]
+----
+
+/*
+ This example shows how to construct and modify
+ URLs to consume a third party API to
+ generate QR Codes.
+ https://developers.google.com/chart/infographics/docs/qr_codes
+*/
+
+#include
+#include
+#include
+#include
+
+namespace urls = boost::urls;
+namespace core = boost::core;
+
+int main(int argc, char** argv)
+{
+ if (argc < 2) {
+ std::cout << argv[0] << "\n";
+ std::cout << "Usage: qrcode