wikidot_path/arguments.rs
1/*
2 * arguments.rs
3 *
4 * wikidot-path - Library to parse Wikidot-like paths.
5 * Copyright (c) 2019-2023 Emmie Maeda
6 *
7 * wikidot-normalize is available free of charge under the terms of the MIT
8 * License. You are free to redistribute and/or modify it under those
9 * terms. It is distributed in the hopes that it will be useful, but
10 * WITHOUT ANY WARRANTY. See the LICENSE file for more details.
11 *
12 */
13
14use super::schema::ArgumentSchema;
15use super::value::ArgumentValue;
16use std::collections::HashMap;
17use unicase::UniCase;
18
19pub type ArgumentKey<'a> = UniCase<&'a str>;
20pub type PageArgumentsMap<'a> = HashMap<ArgumentKey<'a>, (ArgumentValue<'a>, &'a str)>;
21
22/// Represents the set of arguments for a page.
23///
24/// Within a Wikidot-compatible URL, this is the optional portion
25/// *after* a slug. For example:
26///
27/// * `/scp-1000` -- No page arguments.
28/// * `/scp-1000/noredirect/true` -- Page arguments are `/noredirect/true`.
29/// * `/scp-1000/noredirect/true/norender/true` -- Page arguments are `/norender/true/noredirect/true`.
30///
31/// When passed as a string input, the leading `/` character is optional.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct PageArguments<'a>(pub PageArgumentsMap<'a>);
34
35impl<'a> PageArguments<'a> {
36 /// Parse out Wikidot arguments.
37 ///
38 /// This algorithm is compatible with the `/KEY/true` format,
39 /// but also allows a lone `/KEY` for options which are "innately valued",
40 /// such as `norender` or `edit`, where adding a `/true` is not very useful.
41 ///
42 /// This means that for `/KEY1/KEY2/VALUE` where value is not a string
43 /// (i.e. null, boolean, or integer),
44 ///
45 /// If there are duplicate keys, the most recent one takes precedence.
46 pub fn parse(mut path: &'a str, schema: ArgumentSchema) -> Self {
47 // Remove leading slash
48 if path.starts_with('/') {
49 path = &path[1..];
50 }
51
52 // Process each section of the options string into keys and values.
53 let mut arguments = HashMap::new();
54 let mut parts = path.split('/');
55
56 fn process_argument<'a>(
57 arguments: &mut PageArgumentsMap<'a>,
58 key: &'a str,
59 parts: &mut dyn Iterator<Item = &'a str>,
60 schema: ArgumentSchema,
61 ) {
62 let value = parts.next();
63
64 if schema.solo_keys.contains(&key) {
65 // If this potentially is a solo key, then check if the next
66 // value looks like the next key rather than a value.
67
68 if let Some(value) = value {
69 if schema.valid_keys.contains(&value) {
70 // Yield as solo key
71 //
72 // However if we discard 'value' (really the next pair's key)
73 // we will lose data, so we recursively call this function to
74 // handle it.
75
76 let key = ArgumentKey::unicode(key);
77 arguments.insert(key, (ArgumentValue::Null, value));
78 process_argument(arguments, value, parts, schema);
79 return;
80 }
81 }
82 }
83
84 // Otherwise, return as normal key-value pair
85 let key = ArgumentKey::unicode(key);
86 arguments.insert(key, (ArgumentValue::from(value), value.unwrap_or("")));
87 }
88
89 while let Some(key) = parts.next() {
90 if !key.is_empty() {
91 process_argument(&mut arguments, key, &mut parts, schema);
92 }
93 }
94
95 PageArguments(arguments)
96 }
97}