-
-
Save trevordevore/5584753 to your computer and use it in GitHub Desktop.
| /** | |
| * Handlers for converting XML to LiveCode arrays and vice versa. | |
| * | |
| * Provided by Trevor DeVore of Blue Mango Learning Systems. | |
| */ | |
| /** | |
| * \brief Escapes the predefined XML entities in a string. | |
| * | |
| * \param pStr The string to escape the characters in. | |
| * | |
| * \return String | |
| */ | |
| function EscapePredefinedXMLEntities pStr | |
| replace "&" with "&" in pStr | |
| replace "<" with "<" in pStr | |
| replace ">" with ">" in pStr | |
| replace "'" with "'" in pStr | |
| replace quote with """ in pStr | |
| return pStr | |
| end EscapePredefinedXMLEntities | |
| /** | |
| * \brief Unescapes predefined xml entities in a string. | |
| * | |
| * \param pStr The strin to unescape the characters in. | |
| * | |
| * \return String | |
| */ | |
| function UnescapePredefinedXMLEntities pStr | |
| replace "&" with "&" in pStr | |
| replace "<" with "<" in pStr | |
| replace ">" with ">" in pStr | |
| replace "'" with "'" in pStr | |
| replace """ with quote in pStr | |
| return pStr | |
| end UnescapePredefinedXMLEntities | |
| /** | |
| * \brief Helper function for sorting keys of an array based on order in the XML document the array was created from. | |
| * | |
| * \param pKeys List of keys or array whose keys you want to sort. | |
| * \param pStripMetaKeys By default any meta keys (keys starting with "@") will be stripped. Pass in false to bypass this behavior. | |
| * | |
| * LiveCode array keys are never guaranteed to be in order you created them in | |
| * so we must come up with some other way of maintaining proper sequence. | |
| * For arrays representing XML, the XML syntax is used (i.e. node[1], node[2], etc.). | |
| * This handler will sort keys that use this syntax for representing sequence. | |
| * | |
| * \return String | |
| */ | |
| function SortArrayKeysWithXMLOrdering pKeys, pStripMetaKeys | |
| local theKeys | |
| put pStripMetaKeys is not false into pStripMetaKeys | |
| if pKeys is an array then | |
| put the keys of pKeys into pKeys | |
| end if | |
| set the itemDelimiter to "[" | |
| sort pKeys numeric by the last item of each -- 1], 2], 3], etc. | |
| if pStripMetaKeys then | |
| filter pKeys without "@*" | |
| end if | |
| return pKeys | |
| end SortArrayKeysWithXMLOrdering | |
| /** | |
| * \brief Converts an XML tree into a LiveCode multi-dimensional array. | |
| * | |
| * \param pXML The xml to convert. | |
| * \param pStoreEncodedAs Encoding to use. Must be a value that can be passed to uniDecode. Default is "utf8". | |
| * \param pUseValueKey By default node values are stored in a key named after the node. This means you can't have a node with attributes and a node value. Pass in true if you want to store node values in a '@value' key. This will allow a key to have both attributes (in @attributes key) and a value (in @value key). | |
| * \param pForceNumberIndexForNodes A comma delimited list of node names that should always have numbered indexes (NODE[index]) added to them. This makes it easier to loop over results that may have 1 or more results. | |
| * | |
| * A nodes attributes will be stored as an array of it's "@attributes" key. | |
| * Node names will retain the sequence information (i.e. node[1], node[2], etc.). | |
| * This information is necessary to determine order that keys should be processed in. Example: | |
| * set the itemDelimiter to "[" | |
| * put the keys of theArray into theKeys | |
| * sort theKeys numeric by the last item of each | |
| * | |
| * \return Array | |
| */ | |
| function ConvertXMLToArray pXML, pStoreEncodedAs, pUseValueKey, pForceNumberIndexForNodes | |
| local theArray,theResult,theRootNode,theTreeID | |
| local theXMLEncoding | |
| ## Create an XML tree from XML text | |
| put revCreateXMLTree(pXML, true, true, false) into theTreeID | |
| if theTreeID is an integer then | |
| ## Determine the encoding of the XML, default to UTF-8 | |
| put matchText(pXML, "<\?xml (.*)encoding=" & quote & "(.*)" & quote & "\?>", versionMatch, theXMLEncoding) into theResult | |
| if theXMLEncoding is empty then put "utf-8" into theXMLEncoding | |
| ## Now convert to array. | |
| ## The 1st dimension has one key which is the name of the root node. | |
| put revXMLRootNode(theTreeID) into theRootNode | |
| if theRootNode is not empty and not(theRootNode begins with "xmlerr,") then | |
| put ConvertXMLNodeToArray(theTreeID, theRootNode, theXMLEncoding, pStoreEncodedAs, pUseValueKey, pForceNumberIndexForNodes) into theArray[theRootNode] | |
| end if | |
| revDeleteXMLTree theTreeID | |
| end if | |
| return theArray | |
| end ConvertXMLToArray | |
| /** | |
| * \brief Converts and revXML created XML Tree to an array. | |
| * | |
| * \param pXMLTree The xml tree id. | |
| * \param pStoreEncodedAs See docs for ConvertXMLToArray. | |
| * \param pUseValueKey See docs for ConvertXMLToArray. | |
| * \param pForceNumberIndexForNodes See docs for ConvertXMLToArray. | |
| * | |
| * See docs for ConvertXMLToArray. | |
| * | |
| * \return Array | |
| */ | |
| function ConvertXMLTreeToArray pXMLTree, pStoreEncodedAs, pUseValueKey, pForceNumberIndexForNodes | |
| return ConvertXMLToArray(revXMLText(pXMLTree), pStoreEncodedAs, pUseValueKey, pForceNumberIndexForNodes) | |
| end ConvertXMLTreeToArray | |
| /** | |
| * \brief Converts a multi-dimensional array to an XML tree. | |
| * | |
| * \param pArray The array to convert. | |
| * \param pArrayEncoding Encoding used in the array. Must be a value that can be passed to uniEncode. Default is the current platform encoding. | |
| * \param pStoreEncodedAs Encoding to use. Must be a value that can be passed to uniDecode. Default is "utf8". | |
| * | |
| * The array should consist of one key in the 1st dimension. This key becomes the root node in the XML tree. | |
| * Attributes of a node should be stored as an array in an @attributes key. | |
| * Sequence information for multiple nodes with the same name should be included in the node name using brackets (i.e. node[1], node[2], node[3]). | |
| * | |
| * \return XML Tree id (integer) or error message. | |
| */ | |
| function ConvertArrayToXML pArray, pArrayEncoding, pStoreEncodedAs | |
| local theError,theRootNode,theXML,theXMLTree | |
| ## if pArrayEncoding is empty then current platform encoding is assumed | |
| if pStoreEncodedAs is empty then put "UTF-8" into pStoreEncodedAs | |
| ## Create XML for root node. Note that we take extra steps in order to support | |
| ## converting an array that only represents part of a tree rather than the entire tree. | |
| ## In this case there may be multiple nodes at the root level. | |
| put line 1 of the keys of pArray into theRootNode | |
| set the itemDelimiter to "[" | |
| put "<" & item 1 of theRootNode & "/>" into theXML | |
| ## Create XML needed to create tree | |
| put format("<?xml version=\"1.0\" encoding=\"%s\"?>%s", \ | |
| pStoreEncodedAs, theXML) into theXML | |
| put revCreateXMLTree(theXML, true, true, false) into theXMLTree | |
| if theXMLTree is an integer then | |
| ## Loop over all nodes at root level | |
| put false into stripMetaKeys | |
| put SortArrayKeysWithXMLOrdering(the keys of pArray, stripMetaKeys) into theNodes | |
| ## Create tree using helper function | |
| repeat for each line theKey in theNodes | |
| put theKey into theNode | |
| replace space with "-" in theNode | |
| ConvertArrayDimensionToXML pArray[theKey], theXMLTree, slash & theNode, \ | |
| pArrayEncoding, pStoreEncodedAs | |
| put the result into theError | |
| if theError is not empty then exit repeat | |
| end repeat | |
| if theError is not empty then | |
| ## something went wrong, clean bad tree | |
| revDeleteXMLTree theXMLTree | |
| end if | |
| else | |
| put theXMLTree into theError | |
| end if | |
| if theError is not empty then | |
| return theError | |
| else | |
| return theXMLTree | |
| end if | |
| end ConvertArrayToXML | |
| /** | |
| * \brief Helper function for ConvertArrayToXML. | |
| * | |
| * Converts the multi-dimensional array pArray to nodes in pTreeID. Calls itself recursively. | |
| * | |
| * \return Error message. | |
| */ | |
| private command ConvertArrayDimensionToXML pArray, pTreeID, pNode, pArrayEncoding, pStoreEncodedAs | |
| local theError,theKey,theKeys,theNode | |
| ## A workaround for fact that Revolution does not return | |
| ## keys in the order we created them | |
| put false into stripMetaKeys | |
| put SortArrayKeysWithXMLOrdering(the keys of pArray, stripMetaKeys) into theNodes | |
| ## Arrays might have sequencing info in name | |
| ## (i.e. step[1], step[2], ... ) | |
| set the itemDelimiter to "[" | |
| repeat for each line theArrayKey in theNodes | |
| put theArrayKey into theFullNode | |
| replace space with "-" in theFullNode | |
| put item 1 of theFullNode into theNode | |
| ## Look for attributes. These will be added as attributes to pNode. | |
| if theNode is "@attributes" or theNode is "@attr" then | |
| repeat for each line theKey in the keys of pArray[theArrayKey] | |
| put theKey into theAttr | |
| replace space with "-" in theAttr | |
| revSetXMLAttribute pTreeID, pNode, theAttr, \ | |
| EncodeString(pArray[theArrayKey][theKey], \ | |
| pArrayEncoding, pStoreEncodedAs) | |
| if the result begins with "xmlerr," then | |
| put the result && "(setting attribute" && theKey && "for node" && pNode & ")" into theError | |
| end if | |
| if theError is not empty then exit repeat | |
| end repeat | |
| else if theNode is "@value" then | |
| ## This XML tree is using complex structure. Node is the value of the parent node | |
| revPutIntoXMLNode pTreeID, pNode, EncodeString(pArray[theArrayKey], pArrayEncoding, pStoreEncodedAs) | |
| if the result begins with "xmlerr," then | |
| put the result && "(adding child node" && theNode && "to node" && pNode & ")" into theError | |
| end if | |
| else | |
| if the keys of pArray[theArrayKey] is not empty then | |
| ## Node has children. Add node to XML tree then call self recursivly to create children nodes. | |
| revAddXMLNode pTreeID, pNode, theNode, empty | |
| if the result begins with "xmlerr," then | |
| put the result && "(adding node" && theNode & ")" into theError | |
| end if | |
| if theError is empty then | |
| ConvertArrayDimensionToXML pArray[theArrayKey], pTreeID, pNode & slash & theFullNode, \ | |
| pArrayEncoding, pStoreEncodedAs | |
| put the result into theError | |
| end if | |
| else | |
| ## Node has no children but possibly a value. Create node and add value (which may be empty). | |
| revAddXMLNode pTreeID, pNode, theNode, \ | |
| EncodeString(pArray[theArrayKey], pArrayEncoding, pStoreEncodedAs) | |
| if the result begins with "xmlerr," then | |
| put the result && "(adding child node" && theNode && "to node" && pNode & ")" into theError | |
| end if | |
| end if | |
| end if | |
| if theError is not empty then exit repeat | |
| end repeat | |
| return theError | |
| end ConvertArrayDimensionToXML | |
| /** | |
| * \brief Helper function for ConvertXMLToArray. | |
| * | |
| * Converts an XML node to a multi-dimensional array. Calls itself recursively. | |
| * | |
| * \return Array | |
| */ | |
| private function ConvertXMLNodeToArray pTreeID, pNode, pXMLTreeEncoding, pStoreEncodedAs, pUseValueKey, pForceNumberIndexForNodes | |
| local theArrayA,theAttributes,theChildNode,theKey | |
| ## Look for attributes of the node. Store as array in "@attributes" key | |
| put revXMLAttributes(pTreeID, pNode, tab, cr) into theAttributes | |
| if theAttributes is not empty then | |
| put EncodeString(theAttributes, pXMLTreeEncoding, pStoreEncodedAs) into theAttributes | |
| split theAttributes by cr and tab -- create array | |
| put theAttributes into theArrayA["@attributes"] | |
| end if | |
| ## Look for children nodes. | |
| set the itemDelimiter to slash | |
| put revXMLFirstChild(pTreeID, pNode) into theChildNode | |
| if theChildNode is empty or theChildNode begins with "xmlerr," then | |
| put EncodeString(revXMLNodeContents(pTreeID, pNode), pXMLTreeEncoding, pStoreEncodedAs) into theValue | |
| if word 1 to -1 of theValue is empty and the keys of theArrayA is not empty then | |
| ## Empty node that has attributes | |
| return theArrayA | |
| else if pUseValueKey then | |
| ## Force value into @value | |
| put theValue into theArrayA["@value"] | |
| return theArrayA | |
| else | |
| ## Single Node with value: Return value. Attributes are ignored. | |
| return theValue | |
| end if | |
| else | |
| ## Child nodes were found. Recursively call self and store result in array. | |
| set the wholeMatches to true | |
| replace comma with cr in pForceNumberIndexForNodes | |
| repeat while theChildNode is not empty and not (theChildNode begins with "xmlerr,") | |
| put the last item of theChildNode into theKey | |
| if theKey is among the lines of pForceNumberIndexForNodes then | |
| ## Oops, key that needs index doesn't have one. Only 1 entry in XML. | |
| put "[1]" after theKey | |
| end if | |
| put ConvertXMLNodeToArray(pTreeID, theChildNode, pXMLTreeEncoding, pStoreEncodedAs, pUseValueKey, \ | |
| pForceNumberIndexForNodes) into theArrayA[theKey] | |
| put revXMLNextSibling(pTreeID, theChildNode) into theChildNode | |
| end repeat | |
| return theArrayA | |
| end if | |
| end ConvertXMLNodeToArray | |
| /** | |
| * \brief Helper function for converting the encoding of strings when converting to and from XML. | |
| * | |
| * \return String | |
| */ | |
| private function EncodeString pString, pInEncoding, pOutEncoding | |
| ## convert utf-8 to utf8 for uniencode/decode | |
| replace "-" with empty in pInEncoding | |
| replace "-" with empty in pOutEncoding | |
| if pInEncoding is not empty then | |
| -- if pOutEncoding is empty then pString will be converted to the current platform encoding | |
| return uniDecode(uniEncode(pString, pInEncoding), pOutEncoding) | |
| else | |
| if pOutEncoding is not empty then | |
| -- if pInEncoding is empty then pString is assumed to be in the current platform encoding | |
| return uniDecode(uniEncode(pString, pInEncoding), pOutEncoding) | |
| else | |
| return pString | |
| end if | |
| end if | |
| end EncodeString |
The line:
put matchText(pXML, "<?xml (.)encoding=" & quote & "([^" & quote & "])", versionMatch, theXMLEncoding) into theResult
fix the problem.
Hi Trevor. First, thanks for doing this. I am working on a project to import an XML file produced by one of our copiers, edit it, then import it back into the copier. To ensure the entire workflow functions, I imported the file using low level file functions, converted the data to an XML tree, set the arrayData of a tree widget to the array (works great), got the arrayData of the tree view, converted it to an XMLTree, then exported it back to a file. When I compare the files they should IMHO be identical, but they are not. I'd like to send a sample stack if I may, if you are interested in finding out why.
The code
put matchText(pXML, "<?xml (.)encoding=" & quote & "(.)" & quote & "?>", versionMatch, theXMLEncoding) into theResult
applied in string
''
is returning
'encoding="ISO-8859-1" standalone="no'
How to get just
'ISO-8859-1' ?