Rebol [
    Title: "Web Form Encoder/Decoder for Rebol 3"
    Author: "Christopher Ross-Gill"
    Date: 6-Sep-2015
    Home: http://www.ross-gill.com/page/Web_Forms_and_REBOL
    File: %altwebform.r
    Version: 0.10.1
    Purpose: "Convert a Rebol block to a URL-Encoded Web Form string"
    Rights: http://opensource.org/licenses/Apache-2.0
    Type: 'module
    Name: 'rgchris.altwebform
    Exports: [url-decode url-encode load-webform to-webform]
    History: [
        06-Sep-2015 0.10.1 "Add Ruby-style paths to encoding"
        06-Jul-2013  0.9.5 "Fix encoding/decoding of _ character"
        01-Mar-2013  0.9.4 "Detach URL-DECODE and URL-ENCODE"
        27-Feb-2013  0.9.2 "Correct encoding of UTF-8 values"
        18-Nov-2009  0.1.0 "Original Version"
    ]
    Example: [
        "a=3&aa.a=1&b.c=1&b.c=2"
        [a "3" aa [a "1"] b [c ["1" "2"]]]
    ]
]

url-decode: use [as-is hex space][
    as-is: charset ["-.~" #"0" - #"9" #"A" - #"Z" #"a" - #"z"]
    hex: charset [#"0" - #"9" #"a" - #"f" #"A" - #"F"]

    func [
        "Decode percent-encoded text from URLs and Web Forms"
        text [any-string!] "Text to Decode"
        /wiki "Assumes `_` character is used to represent spaces"
    ][
        space: either wiki [#"_"][#"+"]
        either parse text: to binary! text [
            copy text any [
                  some as-is | remove space insert " "
                | [#"_" | #"+" | #"." | #","]
                | change "%0D%0A" "^/" ; de-crlf
                | remove ["%" copy text 2 hex] (text: debase/base text 16) insert text
            ]
        ][to string! text][none]
    ]
]

url-encode: use [as-is space percent-encode][
    as-is: charset ["-." #"0" - #"9" #"A" - #"Z" #"-" #"a" - #"z" #"~"]
    percent-encode: func [text][
        insert next text enbase/base copy/part text 1 16 change text "%"
    ]

    func [
        "Encode text using percent-encoding for URLs and Web Forms"
        text [any-string!] "Text to encode"
        /wiki "Use `_` character to represent spaces"
    ][
        space: either wiki [#"_"][#"+"]
        either parse text: to binary! text [
            copy text any [
                  text: some as-is | end | change " " space
                | [#"_" | #"."] (either wiki [percent-encode text][text])
                | skip (percent-encode text)
            ]
        ][to string! text][""]
    ]
]

load-webform: use [result path string pair as-path term][
    result: copy []

    as-path: func [name [string!]][
        to path! to block! replace/all name #"." #" "
    ]

    path: use [aa an wd][
        aa: charset [#"a" - #"z" #"A" - #"Z" #"_"]
        an: charset [#"-" #"0" - #"9" #"a" - #"z" #"A" - #"Z" #"_"]
        wd: [aa 0 40 an] ; one alpha, any alpha/numeric/dash/underscore
        [wd 0 6 [#"." wd]]
    ]

    string: use [ch hx][
        ch: charset ["-._~" #"0" - #"9" #"A" - #"Z" #"a" - #"z"]
        hx: charset [#"0" - #"9" #"a" - #"f" #"A" - #"F"]
        [any [ch | #"+" | #"%" 2 hx]] ; any [unreserved | percent-encoded]
    ]

    term: [#"&" | end]

    pair: use [name value tree][
        [
            copy name path [
                #"=" copy value string term | term (value: none)
            ] (
                tree: :result
                name: as-path name
                value: url-decode value

                until [
                    tree: any [
                        find/tail tree name/1
                        insert tail tree name/1
                    ]

                    name: next name

                    switch type?/word tree/1 [
                        none! [unless tail? name [insert/only tree tree: copy []]]
                        string! [change/only tree tree: reduce [tree/1]]
                        block! [tree: tree/1]
                    ]

                    if tail? name [append tree value]
                ]
            )
        ]
    ]

    func [
        "Loads data from a URL-Encoded Web Form string"
        webform [string! none!] "Form to decode"
    ][
        webform: any [webform ""]
        result: copy []

        either parse webform [opt [#"&" | #"?"] any pair][result][
            do make error! "Not a URL Encoded Web Form"
        ]
    ]
]

to-webform: use [
    webform form-key emit ruby-style?
    here path reference value block array object
][
    path: []
    form-key: does [
        url-encode rejoin collect [
            keep first path
            foreach key next path [
                keep reduce either ruby-style? [["[" key "]"]][["." key]]
            ]
        ]
    ]

    emit: func [data][
        repend webform ["&" form-key "=" url-encode data]
    ]

    reference: [
        some [
            here: get-word!
            (change/only here attempt [get/any here/1])
            | skip
        ]
    ]

    value: [
          here: number! (emit form here/1)
        | [logic! | 'true | 'false] (emit form here/1)
        | [none! | 'none]
        | date! (replace form date "/" "T")
        | [any-string! | tuple! | money! | time! | pair! | issue!] (emit form here/1)
    ]

    array: [and opt reference any value end]

    object: [
        and opt reference any [
            here: [word! | set-word!] (
                append path to string! to word! here/1
            )
            [value | block] (remove back tail path)
        ] end
    ]

    block: [
        here: and [
              any-block! (change/only here copy here/1)
            | object! (change/only here body-of here/1)
        ] into [object | array]
    ]

    func [
        "Serializes block data as URL-Encoded Web Form string"
        data [block! object!] "Block or object to encode"
        /prefix "Includes the `?` character used to precede URL query strings"
        /ruby-style "Encodes structured keys using `a[b][c]` notation"
    ][
        clear path
        webform: copy ""
        data: either object? data [body-of data][copy data]
        ruby-style?: :ruby-style

        if parse copy data object [
            either all [
                prefix not tail? next webform
            ][
                back change webform "?"
            ][
                remove webform
            ]
        ]
    ]
]