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}