REBOL [
    Title: "REBOL Web Form Encoder/Decoder"
    Author: "Christopher Ross-Gill"
    Date: 30-Aug-2013
    Version: 0.2.0
    Purpose: "Convert a Rebol block to a URL-Encoded Web Form string"
    Comment: "Conforms to application/x-www-form-urlencoded"
    Home: http://www.ross-gill.com/page/RFC_Web_Forms_and_Rebol
    File: %altwebform.r
    Type: 'module
    Exports: [url-decode url-encode load-webform to-webform]
    Example: [
        "a=3&aa.a=1&b.c.=1&b.c.=2"
        [a "3" aa [a "1"] b [c ["1" "2"]]]
    ]
]

url-decode: use [deplus sp decrlf][
    deplus: func [text][
        parse/all text [
            some [to sp text: (text: change text #" ") :text] to end
        ]
        head text
    ]

    decrlf: func [text][
        parse/all text [
            some [to crlf text: (text: change/part text #"^/" 2) :text] to end
        ]
        head text
    ]

    func [text [any-string! none!] /wiki][
        sp: either wiki [#"_"][#"+"]
        decrlf dehex deplus form any [text ""]
    ]
]

load-webform: use [result path string pair as-path multi? pairs?][
    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: union aa charset ["-_" #"0" - #"9"]
        wd: [aa 0 40 an] ; one alpha, any alpha/digit/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]
    ]

    multi?: func [value [none! string! block!]][
        all [
            block? value
            parse value [some string!]
        ]
    ]

    pairs?: func [value [none! string! block!]][
        all [
            block? value
            parse value [some [word! skip]]
        ]
    ]

    pair: use [name value tree is-block?][
        [
            copy name path copy is-block? opt #"."
            #"=" copy value string [#"&" | end]
            (
                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

                    either tail? name [
                        either is-block? [
                            unless multi? tree/1 [
                                change/only tree copy []
                            ]
                            append tree/1 value
                        ][
                            change tree value
                        ]
                        true
                    ][
                        either pairs? tree/1 [
                            tree: tree/1
                        ][
                            change/only tree tree: copy []
                        ]
                        false
                    ]
                ]
            )
        ]
    ]

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

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

url-encode: use [ch sp encode][
    ch: charset ["!'*,-.~" #"0" - #"9" #"A" - #"Z" #"a" - #"z"]
    encode: func [text][insert next text enbase/base form text/1 16 change text "%"]

    func [text [any-string!] /wiki][
        sp: either wiki [#"_"][#"+"]

        parse/all copy text [
            copy text some [
                  text: some ch | #" " (change text sp)
                | #"_" (all [wiki encode text]) | skip (encode text)
            ] | end (text: copy "")
        ]
        text
    ]
]

to-webform: use [
    webform form-key emit in-block?
    here path reference value block array object
][
    path: []

    in-block?: false
    form-key: has [key][
        key: remove head foreach name path [insert "" reduce ["." name]]
        if in-block? [append key "."]
        key
    ]

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

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

    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!] (emit form here/1)
        | [word! | set-word! | lit-word!] (emit form here/1)
        | block
        | some reference value
        | skip ; (emit mold type? here/1)
    ]

    array: [
        (in-block?: true)
        any value end
        (in-block?: false)
    ]

    object: [
        here: any [[word! | set-word!] skip] end :here ; check pair format...
        any [
            here: [word! | set-word!] (insert path to word! here/1)
            value (remove path)
        ] end
    ]

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

    func [
        [catch] "Serializes block data as URL-Encoded Web Form string"
        data [block! object!] /prefix
    ][
        clear path
        webform: copy ""
        data: either object? data [body-of data][copy data]
        throw-on-error [assert [parse copy data object]]
        either all [prefix not tail? next webform][back change webform "?"][remove webform]
    ]
]