My Little TextFile Parser

After exploring XML I am not yet ready to enter the XML realm. JSON, neh. I've never felt the rave for.

As I am dynamically creating HTML content, I wanted something based on a flatfile and something that would enable execution of TCL commands or be rigged to execute whatever based on placing the command to be executed. This introduces the %[command]% syntax.

I used % as an additional tag as I may have dialogue text where by [text may exist within] which I don't want executed as a command.

With this, it enables you to dynamically call a function, retrieve from dictionaries and the likes when rendering parses the file. In this example, I have designed it with elements (easy extensible) one: a fully fledged HTML button, two: text, three: icon.

Which are both assigned to a key; so lets start.

Firstly, we create a prompt script like below

# Texts
# Key | Type (of element) | Icon (to be displayed next to the text) | Text to display
{"%_title_%"},           {"text"}, {""}, {"The example"}
{"%_synopsis_%"}, {"text"}, {""}, {"of an Text File Parser"}

#Buttons
# Key | Type (of element) | Button Name | CSS Class | formaction (url to post to) | value of the button | button text
{"%_button_%"}, {"button"}, {"my_tcl_version"}, {"tcl"}, {"/tcl"}, {"%[info tclversion]%"}, {"TCL Version: %[info tclversion]%"}

# Icons
# Key | Type (of element) | Icon
{"%_icon_%"}, {"icon"}, {"📜"}

Save it somewhere, as we will be calling it within the code below.

Now that we have our prompt file. We can feed it in to our parser: [read_prompt "script"]

proc morphSet {data elements} {
        variable morph
                # loop for the amount of elements in our data
                for { set i 0 } { $i < [llength $data] } {incr i} {
                        # Set the dictionary to the name of our element
                        # Strip the {" and "} from our line_data
                        # Store that to the dictionary
                        dict set morph [lindex $elements $i] [string range [lindex $data $i] 2 end-2]
                } ;# end for
} ;# end proc

proc setDict {data} {
        variable morph ;# Make our dictionary visible to others
        set morph [dict create] ;# Create our dictionary

        # Strip the {" and "} from our line_data of element 1
        set element_type [string range [lindex $data 1] 2 end-2]

        # switch $element_type
        #        Switches between elements
        # Cases: button, text, icon
        #        construct a list of elements to dictonarise
        #         store the line elements to the elements
        #        pass to our dictionary creation procedure
        switch $element_type {
                button {
                        set elements [list element_key        \
                                           element_type       \
                                           element_name       \
                                           element_class      \
                                           element_formaction \
                                           element_value      \
                                           element_text]
                        morphSet $data $elements
                }

                text {
                        set elements [list element_key   \
                                           element_type  \
                                           element_icon  \
                                           element_text]
                        morphSet $data $elements
                }

                icon {
                        set elements [list element_key   \
                                           element_type  \
                                           element_icon]
                        morphSet $data $elements
                }
        } ;#end switch
} ;# end proc

proc generateElement {args} {
    set options {} ;# Initialize an empty dictionary
    foreach {key value} $args {
        set cleanKey [string trimleft $key "-"]  ;# Remove leading "-"
        dict set options $cleanKey $value        ;# Create our key and value
    }

        switch [dict get $options type] {
                button {
                        set button {<button name="[dict get $options name]" class="[dict get $options class]" formaction="[dict get $options formaction]" value="[dict get $options value]">[dict get $options text]</button>}
                        return [string trim [subst "$button"]]
                }
                text {
                        set text {[dict get $options icon] [dict get $options text]}
                        return [string trim [subst "$text"]]
                }
                icon {
                        set icon {[dict get $options icon]}
                        return [string trim [subst "$icon"]]
                }
        } ;# end switch
} ;# end proc

proc evalCode { element data } {
    variable morph

    set pairs {}
    set startIndex 0

    while {1} {
        # Get the index of the opening delimiter [% starting from startIndex
        set startPos [string first "%\[" $data $startIndex]
        # Find the closing delimiter %] starting after the [% index
        set endPos [string first "\]%" $data [expr {$startPos + 1}]]

                # switch $startPos
                # Case: -1
                #        We don't have a valid position
                #        Break
                # Case: default
                #        We located our starting delimiter %[
                #        Create switch
                #
                #                switch $endPos
                #                Case: -1:
                #                        We don't have an ending delimiter
                #                        Break
                #                Case: default
                #                        We have our ending delimiter
                #                        Create our interpreter switch
                #
                #                                switch [string range $tcl_cmd 0 1]
                #                                        Scan the first two characters of our command
                #                                        Allows us to create custom actions based on the first two
                #                                Case: default
                #                                        Create our interpreter as safe
                #                                        store our eval of our command in interpreter
                #                                        Delete our interpreter
                #                                        Replace the data in the string with the returned result

        switch $startPos {
            -1 { break }
            default {
                switch $endPos {
                    -1 { break }
                    default {
                        set tcl_cmd [string range $data $startPos+2 $endPos-1]
                        # With the switch below can check the first two characters
                        # Execute whatever based upon
                        # Reserved for future use
                        switch [string range $tcl_cmd 0 1] {
                            default {
                                #####
                                ## Create our safe-interp
                                set interp [interp create -safe]
                                # Expose our commands
#                               interp alias $interp info {} info
                                ## Execute our command in interpreter
                                set cmd_result [$interp eval $tcl_cmd]
                                ## Destroy
#                               puts "[interp slaves] :: slaves"
                                interp delete $interp ;# Tidy Up
#                               puts "[interp slaves] :: slaves after tidyup"
                                #####
                                set data [string replace $data $startPos $endPos+1 "$cmd_result"]
                            }
                        }
                    }
                }
            }
        } ;# end switch
    } ;# end while
    dict set morph $element "$data"
} ;# end proc

proc composePrompt { } {
    variable morph ;# Gain access to our dictionary

    # This works in conjunction with our PromptFile
    # Are we a button, text? icon?
    # Obtain this from the second element which defines what element we are

    # Switch: $type
    #         Switch to generate our HTML element types
    # Case: button
    #          Setup the HTML button string
    # Case: text
    #          Setup the HTML text string
    # Case: icon
    #          Setup the HTML icon string

    set type "[dict get $morph element_type]"
    ##
    switch $type {
        button {
            set element_key [dict get $morph element_key]
            evalCode  "element_text"  [dict get $morph element_text]
            evalCode  "element_value" [dict get $morph element_value]
            ##
            set element [generateElement \
                                 -name       "[dict get $morph element_name]"       \
                                 -type       "[dict get $morph element_type]"       \
                                 -class      "[dict get $morph element_class]"      \
                                 -formaction "[dict get $morph element_formaction]" \
                                 -value      "[dict get $morph element_value]"      \
                                 -text       "[dict get $morph element_text]"]
            return [list $element_key [string trim "$element"]]
        }

        text {
            set element_key [dict get $morph element_key]
            evalCode   "element_text" [dict get $morph element_text]
            ##
            set element [generateElement \
                                 -type "[dict get $morph element_type]" \
                                 -icon "[dict get $morph element_icon]" \
                                 -text "[dict get $morph element_text]"]
            return [list $element_key [string trim "$element"]]
        }

        icon {
            set element_key [dict get $morph element_key]
            ##
            set element [generateElement \
                                 -type "[dict get $morph element_type]"  \
                                 -icon "[dict get $morph element_icon]"]
            return [list $element_key [string trim "$element"]]
       }

        default { return }
        } ;#end switch
} ;# end procedure

proc read_prompt { file_input } {
    variable morph ;# Get access to our dictonary
    
    # Open our file
    set file [open "$file_input" r]
    while { [gets $file data] >= 0 } {
    # Split each line by a comma
    set parts    [string trimleft [split $data ","]]
    # Get the first occurrence of our {
    set startPos [string first "{" $data]
    # Get the last occurrence of our }
    set endPos   [string last "}" $data]
    # Create an empty list to store our data parts
    set line_data {}

    switch $startPos {
                0 {
                        for { set i 0 } { $i < [llength $parts] } {incr i} {
                                lappend line_data [string trim [lindex $parts $i]]
                        } ;# end for
                }
        } ;# end switch

        setDict $line_data ;# Create our dictionary based on our line_data
        # Switch $morph
        #        is the length of the key is greater than 0, compose our prompt
        # Case: 1
        #        the morph is not empty, has data, append to a list our composed prompt result
        switch [expr {[llength $morph] > 0}] {
                1 { lappend myPrompt [composePrompt] }
        } ;# end switch
} ;# end while
close $file;

# puts "$myPrompt :: elements"
return $myPrompt
} ;# end proc

puts [read_prompt "script"]

Result

{%_title_% {The example}} 
{%_synopsis_% {of an Text File Parser}}
{%_button_% {<button name="my_tcl_version" class="tcl" formaction="/tcl" value="9.0">TCL Version: 9.0</button>}}
{%_icon_% {&#x1F4DC;}}

Add a string map,

set template {%_icon_% %_title_% %_synopsis_%
Coded with: %_button_%}

foreach item [read_prompt "script"] { lappend mapList [lindex $item 0] [lindex $item 1] }
puts [string map $mapList $template]

And now you have something abstract like this:

&#x1F4DC; The example of an Text File Parser
Coded with: <button name="my_tcl_version" class="tcl" formaction="/tcl" value="9.0">TCL Version: 9.0</button>