diff --git a/.gitignore b/.gitignore index bb21aae..95a0227 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,31 @@ src/jsonstringify.ERR *.zip *.PJT *.pjx +scanner.js +src/arraytocursor.BAK +JSONFoxHelper/obj/x86/Release/JSONFoxHelper.pdb +JSONFoxHelper/obj/x86/Release/JSONFoxHelper.dll +jsonfox_ref.FPT +jsonfox_ref.DBF +jsonfox_ref.CDX +JSONFoxHelper/.vs/JSONFoxHelper/FileContentIndex/ce6bbe6c-b547-4029-a444-eb439cf20c46.vsidx +JSONFoxHelper/.vs/JSONFoxHelper/FileContentIndex/read.lock +JSONFoxHelper/.vs/JSONFoxHelper/v16/.suo +JSONFoxHelper/.vs/JSONFoxHelper/v17/.suo +JSONFoxHelper/bin/Debug/JSONFoxHelper.dll +JSONFoxHelper/bin/Debug/JSONFoxHelper.pdb +JSONFoxHelper/bin/Debug/JSONFoxHelper.tlb +JSONFoxHelper/bin/Release/JSONFoxHelper.dll +JSONFoxHelper/bin/Release/JSONFoxHelper.pdb +JSONFoxHelper/bin/x86/Debug/JSONFoxHelper.dll +JSONFoxHelper/bin/x86/Debug/JSONFoxHelper.pdb +JSONFoxHelper/bin/x86/Debug/JSONFoxHelper.tlb +JSONFoxHelper/bin/x86/Release/JSONFoxHelper.dll +JSONFoxHelper/bin/x86/Release/JSONFoxHelper.pdb +JSONFoxHelper/obj/x86/Release/DesignTimeResolveAssemblyReferences.cache +JSONFoxHelper/obj/x86/Release/DesignTimeResolveAssemblyReferencesInput.cache +JSONFoxHelper/obj/x86/Release/JSONFoxHelper.csproj.AssemblyReference.cache +JSONFoxHelper/obj/x86/Release/JSONFoxHelper.csproj.CoreCompileInputs.cache +JSONFoxHelper/obj/x86/Release/JSONFoxHelper.csproj.FileListAbsolute.txt +*.BAK +*.FXP diff --git a/JSONFox.PJT b/JSONFox.PJT index 347ae8a..b656203 100644 Binary files a/JSONFox.PJT and b/JSONFox.PJT differ diff --git a/JSONFox.pjx b/JSONFox.pjx index d3af685..7057ad0 100644 Binary files a/JSONFox.pjx and b/JSONFox.pjx differ diff --git a/README.md b/README.md index 451efb0..4ce482a 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,10 @@ **JSONFox** is a free **JSON / XML** ***parser*** for Visual FoxPro 9.0 -Si te gusta mi trabajo puedes apoyarme con un donativo: -[![DONATE!](http://www.pngall.com/wp-content/uploads/2016/05/PayPal-Donate-Button-PNG-File-180x100.png)](https://www.paypal.com/donate/?hosted_button_id=LXQYXFP77AD2G) +Formas de apoyar: +1. Donativo en Paypal [![DONATE!](http://www.pngall.com/wp-content/uploads/2016/05/PayPal-Donate-Button-PNG-File-180x100.png)](https://www.paypal.com/donate/?hosted_button_id=LXQYXFP77AD2G) +2. Patrocinio en [Patreon](www.patreon.com/IrwinRodriguez) +3. Seguir este proyecto (es gratis) [Stargazer](https://github.com/Irwin1985/JSONFox/stargazers) Gracias por tu apoyo! @@ -66,10 +68,13 @@ Insert into cGames Values('The Legend of Zelda', 1986) ?_Screen.Json.CursorStructure('cGames') ``` ## Full Documentation -* ![](docs/meth.gif) **_Screen.Json.CursorToJSON(tcCursor As String *[, tbCurrentRow As Boolean [, tnDataSession As Integer]]*)** +* ![](docs/meth.gif) **_Screen.Json.CursorToJSON(tcCursor As String *[, tbCurrentRow, tnDataSession, tbJustArray, tbParseUTF8, tbTrimChars]*)** * ![](docs/prop.gif) **tcCursor:** the name of your cursor. * ![](docs/prop.gif) **tbCurrentRow:** ¿Would you like to serialize the current row? .F. as default. * ![](docs/prop.gif) **tnDataSession:** Provide this parameter if you're working in a private session. +* ![](docs/prop.gif) **tbJustArray:** if is set to .T. then you get just an array with the data, otherwise you'll get an object containing both the cursor name and the array data. +* ![](docs/prop.gif) **tbParseUTF8:** if is set to .T. then all special characters will be encoded. Eg: 'é' => '\u00e9' +* ![](docs/prop.gif) **tbTrimChars:** if is set to .T. then all right blank spaces will be trimed.
@@ -205,3 +210,7 @@ _Screen.Json.JSONViewer(lcStr) ``` ![](docs/sample1.png) ![](docs/sample2.png) + +**SIN GARANTÍA** + +EL SOFTWARE SE PROPORCIONA "TAL CUAL", SIN GARANTÍA DE NINGÚN TIPO, EXPRESA O IMPLÍCITA, INCLUYENDO PERO NO LIMITADO A LAS GARANTÍAS DE COMERCIABILIDAD, IDONEIDAD PARA UN PROPÓSITO PARTICULAR Y NO INFRACCIÓN. EN NINGÚN CASO LOS AUTORES O TITULARES DE DERECHOS DE AUTOR SERÁN RESPONSABLES DE NINGÚN RECLAMO, DAÑO U OTRA RESPONSABILIDAD, YA SEA EN UNA ACCIÓN CONTRACTUAL, AGRAVIO O DE OTRA MANERA, QUE SURJA DE, FUERA DE O EN RELACIÓN CON EL SOFTWARE O EL USO U OTRAS NEGOCIOS EN EL SOFTWARE. diff --git a/jsonfox.app b/jsonfox.app index 56a373f..716c7c5 100644 Binary files a/jsonfox.app and b/jsonfox.app differ diff --git a/src/arraytocursor.prg b/src/arraytocursor.prg index 1b89124..38a6580 100644 --- a/src/arraytocursor.prg +++ b/src/arraytocursor.prg @@ -10,10 +10,10 @@ Define Class ArrayToCursor As Session Dimension aRows(1) nRowCount = 0 - Dimension tokens[1] Hidden current Hidden previous Hidden peek + hidden tokenCollection Hidden capacity Hidden length @@ -22,8 +22,8 @@ Define Class ArrayToCursor As Session function init(toScanner) With this Local laTokens - laTokens = toScanner.scanTokens() - =Acopy(laTokens, .tokens) + .tokenCollection = toScanner.scanTokens() + .current = 1 .oTableStruct = Createobject('Collection') @@ -59,6 +59,7 @@ Define Class ArrayToCursor As Session Dimension .aRows[.capacity] .InsertData() + .CleanUp() endwith EndFunc @@ -114,38 +115,48 @@ Define Class ArrayToCursor As Session && ======================================================================== && Hidden Function kvp With this - Local lcProp, lxValue, lcType, lnFieldLength, lcFieldName, loPair + Local lcProp, lxValue, lcType, lnFieldLength, lcFieldName, lnDecimals, loPair .consume(T_STRING, "Expect right key element") lcProp = .previous.value - .consume(T_COLON, "Expect ':' after key element.") lcFieldName = Space(1) lxValue = .Value() - lcType = Vartype(lxValue) + lcType = lxValue.Type lnFieldLength = 0 + lnDecimals = 0 Do Case Case lcType == 'N' - lcType = Iif(Occurs('.', Transform(lxValue)) > 0, 'N', 'I') + local lcNumStr + lcNumStr = lxValue.Literal + lcType = Iif(Occurs('.', lcNumStr) > 0 or lxValue.Value > INTEGER_MAX_CAPACITY, 'N', 'I') + && >>>>>>> IRODG 03/17/24 + If lcType == 'N' + lnFieldLength = Len(lcNumStr) + lnDecimals = len(GetWordNum(lcNumStr,2,'.')) + EndIf + && <<<<<<< IRODG 03/17/24 Case lcType == 'C' - If Len(lxValue) > STRING_MAX_SIZE + If Len(lxValue.Value) > STRING_MAX_SIZE lcType = 'M' Else - lxValue = _Screen.JSONUtils.CheckString(lxValue) - lcType = Vartype(lxValue) + lxValue.Value = _Screen.JSONUtils.CheckString(lxValue.Value) + lcType = Vartype(lxValue.Value) Endif If lcType == 'C' - lnFieldLength = Iif(Empty(Len(lxValue)), 1, Len(lxValue)) + lnFieldLength = Iif(Empty(Len(lxValue.Value)), 1, Len(lxValue.Value)) Endif Endcase lcFieldName = Lower(_Screen.JSONUtils.CheckProp(lcProp)) - .CheckStructure(lcFieldName, lcType, lnFieldLength) + .CheckStructure(lcFieldName, lcType, lnFieldLength, lnDecimals) * Set Key-Value pair object loPair = CreateObject("Empty") =AddProperty(loPair, "field", lcFieldName) - =AddProperty(loPair, "value", lxValue) + =AddProperty(loPair, "value", lxValue.Value) + + release lxValue Return loPair EndWith @@ -156,25 +167,37 @@ Define Class ArrayToCursor As Session && ======================================================================== && Hidden Function Value As Variant With this + local loToken + loToken = createobject("Empty") + addproperty(loToken, "type", "") + addproperty(loToken, "literal", "") + addproperty(loToken, "value", .null.) + Do Case Case .match(T_STRING) - Return .previous.value - + loToken.type = 'C' + loToken.literal = .previous.value + loToken.value = .previous.value case .match(T_NUMBER) Local lcValue lcValue = .previous.value - return iif(at('.', lcValue) > 0, Val(lcValue), int(Val(lcValue))) + loToken.type = 'N' + loToken.literal = .previous.value + loToken.value = iif(at('.', lcValue) > 0, Val(lcValue), int(Val(lcValue))) case .match(T_BOOLEAN) - return (.previous.value == 'true') - + loToken.type = 'L' + loToken.literal = .previous.value + loToken.value = (.previous.value == 'true') case .match(T_NULL) - return .null. - + loToken.type = 'X' + loToken.literal = 'null' + loToken.value = .null. Otherwise Error "Parser Error: This token is invalid in for cursor conversion: '" + _screen.jsonUtils.tokenTypeToStr(.peek.type) + "'" - EndCase - EndWith + endcase + endwith + return loToken Endfunc * InsertData Hidden Function InsertData @@ -218,8 +241,8 @@ Define Class ArrayToCursor As Session loPair.FieldType = 'L' Case loPair.FieldType == 'C' lcFlags = '(' + Alltrim(Str(loPair.FieldLength)) + ')' - Case loPair.FieldType == 'N' - lcFlags = "(18,5)" && Integer part 18 and Decimal part 5 + Case loPair.FieldType == 'N' + lcFlags = "("+Alltrim(Str(loPair.FieldLength))+","+Alltrim(Str(loPair.FieldDecimals))+")" && Integer part 18 and Decimal part 5 EndCase cQuery = cQuery + loPair.FieldType + lcFlags + " NULL" EndFor @@ -235,9 +258,9 @@ Define Class ArrayToCursor As Session * CheckStructure * Adds or Updates an entry key in the oTableStruct dictionary * ============================================================= * - Function CheckStructure(tcFieldName, tcType, tnLength) + Function CheckStructure(tcFieldName, tcType, tnLength, tnDecimals) With this - Local nFieldIdx, loPair + Local nFieldIdx, loPair, lbUpdate nFieldIdx = .oTableStruct.GetKey(tcFieldName) If nFieldIdx > 0 loPair = .oTableStruct.Item(nFieldIdx) @@ -256,13 +279,41 @@ Define Class ArrayToCursor As Session * Remove the key and registry a new one .oTableStruct.Remove(nFieldIdx) .oTableStruct.Add(loPair, tcFieldName) - Endif + EndIf + && >>>>>>> IRODG 03/17/24 + case loPair.fieldType == 'N' && Check integer and decimal part length (always saves the longest) + If tnLength > loPair.fieldLength + loPair.fieldLength = tnLength + lbUpdate = .T. + EndIf + If tnDecimals > loPair.fieldDecimals + loPair.fieldDecimals = tnDecimals + lbUpdate = .T. + EndIf + If lbUpdate + * Remove the key and registry a new one + .oTableStruct.Remove(nFieldIdx) + .oTableStruct.Add(loPair, tcFieldName) + EndIf + Case loPair.fieldType == 'I' and tcType == 'N' && Check string length (always saves the longest) + loPair.fieldType = tcType + If tnLength > loPair.fieldLength + loPair.fieldLength = tnLength + EndIf + If tnDecimals > loPair.fieldDecimals + loPair.fieldDecimals = tnDecimals + EndIf + * Remove the key and registry a new one + .oTableStruct.Remove(nFieldIdx) + .oTableStruct.Add(loPair, tcFieldName) + && <<<<<<< IRODG 03/17/24 EndCase Else * Insert new field. loPair = CreateObject('Empty') AddProperty(loPair, 'fieldType', tcType) AddProperty(loPair, 'fieldLength', tnLength) + AddProperty(loPair, 'fieldDecimals', tnDecimals) .oTableStruct.Add(loPair, tcFieldName) EndIf EndWith @@ -304,7 +355,7 @@ Define Class ArrayToCursor As Session If !.isAtEnd() .current = .current + 1 EndIf - Return .tokens[.current-1] + Return .TokenCollection.tokens[.current-1] EndWith endfunc @@ -316,14 +367,28 @@ Define Class ArrayToCursor As Session Hidden Function peek_access With this - Return .tokens[.current] + Return .TokenCollection.tokens[.current] endwith endfunc Hidden Function previous_access With this - Return .tokens[.current-1] + Return .TokenCollection.tokens[.current-1] EndWith EndFunc + function CleanUp + if type('this.aRows',1) == 'A' and alen(this.aRows) > 0 + local i + for i=1 to alen(this.aRows) + this.aRows[i] = .null. + next + dimension this.aRows[1] + this.aRows[1] = .null. + endif + this.oTableStruct = .null. + this.length = 1 + this.capacity = 0 + endfunc + Enddefine diff --git a/src/cursortoarray.prg b/src/cursortoarray.prg index 3b414ea..265e8df 100644 --- a/src/cursortoarray.prg +++ b/src/cursortoarray.prg @@ -2,6 +2,9 @@ define class CursorToArray as session nSessionID = 0 CurName = "" + ParseUTF8 = .f. + TrimChars = .F. + * Function CursorToArray function CursorToArray as memo if !empty(this.nSessionID) @@ -9,7 +12,17 @@ define class CursorToArray as session endif private JSONUtils JSONUtils = _screen.JSONUtils - local lcOutput as memo, i as Integer + local lcOutput as memo, ; + i as Integer, ; + lcValue as Variant, ; + llCentury as Boolean, ; + llDeleted as Boolean, ; + lcDateAct as string, ; + nCounter as Integer, ; + lnTotField as Integer, ; + lnTotal as Integer, ; + lnRecNo as Integer + lcOutput = "[" llCentury = set("Century") == "OFF" llDeleted = set("Deleted") == "OFF" @@ -37,7 +50,7 @@ define class CursorToArray as session lcValue = evaluate(.CurName + "." + aColumns[i, 1]) if vartype(lcValue) = 'X' lcValue = "null" - lcOutput = lcOutput + alltrim(lcValue) + lcOutput = lcOutput + lcValue else do case case aColumns[i, 2] $ "CDTBGMQVW" @@ -54,12 +67,12 @@ define class CursorToArray as session else lcValue = 'null' endif - otherwise - lcValue = JSONUtils.GetString(alltrim(lcValue)) + Otherwise + lcValue = JSONUtils.GetString(Iif(this.TrimChars, Alltrim(lcValue), lcValue), this.ParseUTF8) endcase - lcOutput = lcOutput + alltrim(lcValue) + lcOutput = lcOutput + Iif(this.TrimChars, Alltrim(lcValue), lcValue) case aColumns[i, 2] $ "YFIN" - lcOutput = lcOutput + transform(lcValue) + lcOutput = lcOutput + alltrim(transform(lcValue, "@T")) case aColumns[i, 2] = "L" lcOutput = lcOutput + iif(lcValue, "true", "false") endcase diff --git a/src/cursortojsonobject.prg b/src/cursortojsonobject.prg new file mode 100644 index 0000000..bfb86d8 --- /dev/null +++ b/src/cursortojsonobject.prg @@ -0,0 +1,95 @@ +* CursorToJsonObject Parser +define class CursorToJsonObject as session + nSessionID = 0 + CurName = "" + ParseUTF8 = .f. + TrimChars = .F. + + * Function CursorToArray + function CursorToJsonObject as object + if !empty(this.nSessionID) + set datasession to this.nSessionID + EndIf + Local laArray, loRow, lnRecno + laArray = createobject("TParserInternalArray") + + select (this.CurName) + lnRecno = Recno() + Scan + Scatter memo name loRow + laArray.Push(loRow) + EndScan + + Try + Go lnRecno + Catch + EndTry + return @laArray.getArray() + EndFunc + + * MasterDetail + Function MasterDetailToJSON(tcMaster as String, tcDetail as String, tcExpr as String, tcDetailAttribute as String, tnSessionID as Integer) as Object + if !empty(tnSessionID) + set datasession to tnSessionID + ENDIF + + Local laArray, loRow, lnRecno, lbContinue, laDetail, lcMacro, lcCursor,i, lcDetailFields, lcMacro + laArray = createobject("TParserInternalArray") + ** Set Detail cursor + this.nSessionID = tnSessionID + lcDetailFields = "*" + If At("<", tcDetail) > 0 + lcDetailFields = Alltrim(strextract(tcDetail,"<",">")) + tcDetail = Alltrim(GetWordNum(tcDetail,1,"<")) + EndIf + + select (tcMaster) + lnRecno = Recno() + i = 0 + scan + i = i + 1 + Scatter memo name loRow + laDetail = .null. + * Filter detail + lcCursor = Sys(2015) + try + lcMacro = "Select " + lcDetailFields + " from " + tcDetail + " where " + tcExpr + " into cursor " + lcCursor + &lcMacro + lbContinue = Used(lcCursor) + Catch + lbContinue = .f. + EndTry + If !lbContinue + Loop + EndIf + + If Reccount(lcCursor) > 0 + this.curName = lcCursor + laDetail = this.CursorToJsonObject() + Local array laRows[1] + Acopy(laDetail, laRows) + lcMacro = 'AddProperty(loRow, "'+tcDetailAttribute+'[1]", .null.)' + &lcMacro + lcMacro = 'Acopy(laRows, loRow.'+tcDetailAttribute+')' + &lcMacro + Else + lcMacro = 'AddProperty(loRow, "'+tcDetailAttribute+'", .null.)' + &lcMacro + EndIf + + Use in (lcCursor) + + laArray.Push(loRow) + EndScan + + Try + Go lnRecno in (tcMaster) + Catch + EndTry + IF i>0 + return @laArray.getArray() + ELSE + RETURN .null. + ENDIF + EndFunc +enddefine \ No newline at end of file diff --git a/src/frmjsonviewer.SCT b/src/frmjsonviewer.SCT index fb8a8e5..e6c05a9 100644 Binary files a/src/frmjsonviewer.SCT and b/src/frmjsonviewer.SCT differ diff --git a/src/frmjsonviewer.scx b/src/frmjsonviewer.scx index 0b3951e..6ee9395 100644 Binary files a/src/frmjsonviewer.scx and b/src/frmjsonviewer.scx differ diff --git a/src/jscriptscanner.prg b/src/jscriptscanner.prg new file mode 100644 index 0000000..a9e5fba --- /dev/null +++ b/src/jscriptscanner.prg @@ -0,0 +1,284 @@ +#include "JSONFox.h" +* JScriptScanner +define class JScriptScanner as custom + Hidden source + hidden line + + Hidden capacity + Hidden length + + Dimension tokens[1] + oScript = .null. + + function init(tcSource) + With this + .length = 1 + .capacity = 0 + && IRODG 11/08/2023 Inicio + * We remove possible invalid characters from the input source. + tcSource = STRTRAN(tcSource, CHR(0)) + tcSource = STRTRAN(tcSource, CHR(10)) + tcSource = STRTRAN(tcSource, CHR(13)) + && IRODG 11/08/2023 Fin + .source = tcSource + .line = 1 + endwith + endfunc + + function escapeCharacters(tcLexeme) + * Convert all escape sequences + tcLexeme = Strtran(tcLexeme, '\\', '\') + tcLexeme = Strtran(tcLexeme, '\/', '/') + tcLexeme = Strtran(tcLexeme, '\n', Chr(10)) + tcLexeme = Strtran(tcLexeme, '\r', Chr(13)) + tcLexeme = Strtran(tcLexeme, '\t', Chr(9)) + tcLexeme = Strtran(tcLexeme, '\"', '"') + tcLexeme = Strtran(tcLexeme, "\'", "'") + return tcLexeme + endfunc + + procedure increaseNewLine + this.line = this.line + 1 + endproc + + function checkUnicodeFormat(tcLexeme) + * Look for unicode format + ** This conversion is better (in performance) than Regular Expressions. + && IRODG 09/10/2023 Inicio + local lcUnicode, lcConversion, lbReplace, lnPos + lnPos = 1 + do while .T. + lbReplace = .F. + lcUnicode = substr(tcLexeme, at('\u', tcLexeme, lnPos), 6) + if len(lcUnicode) == 6 + lbReplace = .T. + else + lcUnicode = substr(tcLexeme, at('\U', tcLexeme, lnPos), 6) + if len(lcUnicode) == 6 + lbReplace = .T. + endif + endif + if lbReplace + tcLexeme = strtran(tcLexeme, lcUnicode, strtran(strconv(lcUnicode,16), chr(0))) + else + exit + endif + enddo + && IRODG 09/10/2023 Fin + return tcLexeme + endfunc + + Function scanTokens + With this + Dimension .tokens[1] + + this.oScript = Createobject([MSScriptcontrol.scriptcontrol.1]) + this.oScript.Language = "JScript" + *this.oScript.AddCode(strconv(filetostr('F:\Desarrollo\GitHub\JSONFox\scanner.js'),11)) + local lcScript + lcScript = this.loadScript() + + * _cliptext = lcScript + * messagebox(lcScript) + + this.oScript.AddCode(lcScript) + this.oScript.AddObject("oScanner", this) + this.oScript.Run("ScanTokens", this.source) + .capacity = .length-1 + * Shrink array + Dimension .tokens[.capacity] + endwith + Return @this.tokens + endfunc + + function log(tcContent) + ? tcContent + strtofile(tcContent + CRLF, 'f:\desarrollo\github\jsonfox\trace.log', 1) + endfunc + + function addToken(tnTokenType, tcTokenValue) + With this + .checkCapacity() + local loToken + loToken = createobject("Empty") + =addproperty(loToken, "type", tnTokenType) + =addproperty(loToken, "value", tcTokenValue) + =AddProperty(loToken, "line", .line) + + .tokens[.length] = loToken + .length = .length + 1 + EndWith + EndFunc + + Hidden function checkCapacity + With this + If .capacity < .length + 1 + If Empty(.capacity) + .capacity = 8 + Else + .capacity = .capacity * 2 + EndIf + Dimension .tokens[.capacity] + EndIf + endwith + endfunc + + procedure showError(tcCharacter, tnCurrent) + local lcMessage + lcMessage = "Unknown character ['" + transform(tcCharacter) + "'], ascii: [" + TRANSFORM(ASC(tcCharacter)) + "]" + error "SYNTAX ERROR: (" + TRANSFORM(this.line) + ":" + TRANSFORM(tnCurrent) + ")" + lcMessage + endproc + + function tokenStr(toToken) + local lcType, lcValue + lcType = _screen.jsonUtils.tokenTypeToStr(toToken.type) + lcValue = alltrim(transform(toToken.value)) + return "Token(" + lcType + ", '" + lcValue + "') at Line(" + Alltrim(Str(toToken.Line)) + ")" + endfunc + + function loadScript + local lcScript + text to lcScript noshow +var C_LBRACE = 1 +var C_RBRACE = 2 +var C_LBRACKET = 3 +var C_RBRACKET = 4 +var C_COMMA = 5 +var C_COLON = 6 +var C_NULL = 9 +var C_NUMBER = 10 +var C_STRING = 12 +var C_EOF = 17 +var C_BOOLEAN = 18 +var C_NEWLINE = 19 + +var Spec = [ + // -------------------------------------- + // Whitespace: + [/^[ \t\r\f]/, null], + + // -------------------------------------- + // New line: + [/^\n/, C_NEWLINE], + + // -------------------------------------- + // Keywords + [/^\btrue\b/, C_BOOLEAN], + [/^\bfalse\b/, C_BOOLEAN], + [/^\bnull\b/, C_NULL], + + // -------------------------------------- + // Symbols + [/^\{/, C_LBRACE], + [/^\}/, C_RBRACE], + [/^\[/, C_LBRACKET], + [/^\]/, C_RBRACKET], + [/^\:/, C_COLON], + [/^\,/, C_COMMA], + + // -------------------------------------- + // Numbers: + [/^-?\d+(,\d{3})*(\.\d+)?([eE][-+]?\d+)?/, C_NUMBER], + + // -------------------------------------- + // Double quoted string: + [/^"/, C_STRING] +]; + +var _scannerString; +var _scannerCursor; + +function ScanTokens(source) { + _scannerString = source; + _scannerCursor = 0; // track the position of each character + + while (_scannerCursor < _scannerString.length) { + var token = _getNextToken(); + if (token == null) { + break; + } + oScanner.AddToken(token.type, token.value); + } + oScanner.AddToken(C_EOF, ''); +} + +function _getNextToken() { + if (_scannerCursor >= _scannerString.length) { + return null; + } + var string = _scannerString.slice(_scannerCursor); + + for (var i = 0; i < Spec.length; i++) { + var regexp = Spec[i][0]; + var tokenType = Spec[i][1]; + var tokenValue = _matchRegEx(regexp, string); + + if (tokenValue == null) { + continue; + } + + if (tokenType == null) { + return _getNextToken(); + } + + if (tokenType === C_NEWLINE) { + oScanner.increaseNewLine(); + return _getNextToken(); + } + var literal = tokenValue; + if (tokenType === C_STRING) { + literal = _parseString(); + } + + return { + type: tokenType, + value: literal + }; + } + + oScanner.showError(string[0], _scannerCursor); +} + +function _matchRegEx(regexp, string) { + var matched = regexp.exec(string); + if (matched == null) { + return null; + } + _scannerCursor += matched[0].length; + return matched[0]; +} + +function _parseString() { + var ch = ''; + var looping = true; + var start = _scannerCursor-1; + var pn = ''; + while (_scannerCursor < _scannerString.length) { + ch = _scannerString.charAt(_scannerCursor); + switch (ch) { + case '\\': + pn = (_scannerCursor+1 <= _scannerString.length) ? _scannerString.charAt(_scannerCursor+1) : ''; + if (pn === '\\' || pn === '/' || pn === 'n' || pn === 'r' || pn === 't' || pn === '"' || pn === "'") { + _scannerCursor++; + } + break; + case '"': + looping = false; + break; + default: + break; + } + _scannerCursor++; + if (!looping) { + break; + } + } + var lexeme = _scannerString.slice(start+1, _scannerCursor-1); + lexeme = oScanner.escapeCharacters(lexeme); + lexeme = oScanner.checkUnicodeFormat(lexeme); + return lexeme; +} + endtext + return lcScript + endfunc +enddefine \ No newline at end of file diff --git a/src/jsonclass.prg b/src/jsonclass.prg index 06a4a55..006c9fe 100644 --- a/src/jsonclass.prg +++ b/src/jsonclass.prg @@ -4,16 +4,20 @@ define class JSONClass as session LastErrorText = "" lError = .f. lShowErrors = .t. - version = "9.7" + version = "12.2" hidden lInternal hidden lTablePrompt - Dimension aCustomArray[1] - && >>>>>>> IRODG 07/01/21 - * Set this property to .T. if you want the lexer uses JSONFoxHelper.dll + dimension aCustomArray[1] +&& >>>>>>> IRODG 07/01/21 +* Set this property to .T. if you want the lexer uses JSONFoxHelper.dll NETScanner = .f. - && <<<<<<< IRODG 07/01/21 +&& <<<<<<< IRODG 07/01/21 - *Function Init +&& >>>>>>> IRODG 02/27/24 + JScriptScanner = .f. +&& <<<<<<< IRODG 02/27/24 + +*Function Init function init with this .ResetError() @@ -22,90 +26,150 @@ define class JSONClass as session endwith endfunc - * Parse the string text as JSON +* Parse the string text as JSON function Parse as memo lparameters tcJsonStr as memo - local loJSONObj + local loJSONObj&&, loEnv loJSONObj = .null. - Dimension this.aCustomArray[1] - this.aCustomArray[1] = .Null. + dimension this.aCustomArray[1] + this.aCustomArray[1] = .null. try + &&loEnv = this.saveEnvironment() this.ResetError() local lexer, parser - if this.NETScanner + do case + case this.NETScanner lexer = createobject("NetScanner", tcJsonStr) - else + case this.JScriptScanner + lexer = createobject("JScriptScanner", tcJsonStr) + otherwise lexer = createobject("Tokenizer", tcJsonStr) - endif + endcase parser = createobject("Parser", lexer) loJSONObj = parser.Parse() - catch to loEx + if type('lexer') == 'O' + lexer.CleanUp() + endif + + if type('parser') == 'O' + parser.CleanUp() + endif this.ShowExceptionError(loEx) finally + &&this.restoreEnvironment(loEnv) store .null. to lexer, parser release lexer, parser - ENDTRY - If Type('loJSONObj', 1) == 'A' - Local i - For i = 1 to Alen(loJSONObj, 1) - Dimension this.aCustomArray[i] + endtry + if type('loJSONObj', 1) == 'A' + local i + for i = 1 to alen(loJSONObj, 1) + dimension this.aCustomArray[i] this.aCustomArray[i] = loJSONObj[i] endfor return @this.aCustomArray - Else + else return loJSONObj - EndIf + endif endfunc - * Stringify +* tokenize + function dumpTokens + lparameters tcJsonStr as memo, tcOutput as string + &&local loEnv + try + &&loEnv = this.saveEnvironment() + this.ResetError() + local lexer, nativeScanner + do case + case this.NETScanner + lexer = createobject("NetScanner", tcJsonStr) + case this.JScriptScanner + lexer = createobject("JScriptScanner", tcJsonStr) + otherwise + nativeScanner = .t. + lexer = createobject("Tokenizer", tcJsonStr) + endcase + local laTokenCollection + laTokenCollection = lexer.scanTokens() + if file(tcOutput) + delete file (tcOutput) + endif + for each loToken in laTokenCollection.Tokens + strtofile(tokenStr(loToken), tcOutput, 1) + endfor + catch to loEx + if type('lexer') == 'O' and nativeScanner + lexer.CleanUp() + endif + this.ShowExceptionError(loEx) + finally + &&this.restoreEnvironment(loEnv) + release lexer, laTokenCollection + endtry + endfunc + +* Stringify function Stringify as memo - lparameters tvNewVal as Variant, tcFlags as string, tlParseUtf8 + lparameters tvNewVal as Variant, tcFlags as string, tlParseUtf8 as Boolean, tlTrimChars as Boolean this.ResetError() - local llParseUtf8, lcTypeFlag, loJSONStr as memo + local llParseUtf8, lcTypeFlag, loJSONStr as memo&&, loEnv lcTypeFlag = type('tcFlags') llParseUtf8 = iif(lcTypeFlag = 'L', tcFlags, tlParseUtf8) loJSONStr = "" - if vartype(tvNewVal) = "O" try + &&loEnv = this.saveEnvironment() local objToJson objToJson = createobject("ObjectToJson") tvNewVal = objToJson.Encode(@tvNewVal, iif(lcTypeFlag != 'C', .f., tcFlags)) catch to loEx this.ShowExceptionError(loEx) finally + &&this.restoreEnvironment(loEnv) objToJson = .null. release objToJson endtry endif - try + &&loEnv = this.saveEnvironment() local lexer, parser lexer = createobject("Tokenizer", tvNewVal) parser = createobject("JSONStringify", lexer) - loJSONStr = parser.Stringify(llParseUtf8) + loJSONStr = parser.Stringify(llParseUtf8, tlTrimChars) catch to loEx + if type('lexer') == 'O' + lexer.CleanUp() + endif + + if type('parser') == 'O' + parser.CleanUp() + endif this.ShowExceptionError(loEx) finally + &&this.restoreEnvironment(loEnv) store .null. to lexer, parser release lexer, parser endtry return loJSONStr endfunc - * JSONToRTF +* JSONToRTF function JSONToRTF as memo lparameters tvNewVal as Variant, tnIndent as Boolean + &&local loEnv + this.ResetError() if vartype(tvNewVal) = 'O' try + &&loEnv = this.saveEnvironment() local objToJson objToJson = createobject("ObjectToJson") tvNewVal = objToJson.Encode(@tvNewVal) catch to loEx this.ShowExceptionError(loEx) finally + &&this.restoreEnvironment(loEnv) objToJson = .null. release objToJson endtry @@ -113,6 +177,7 @@ define class JSONClass as session local loJSONStr as memo loJSONStr = '' try + &&loEnv = this.saveEnvironment() this.lError = .f. this.LastErrorText = '' local lexer, parser @@ -123,17 +188,25 @@ define class JSONClass as session this.lError = parser.lError this.LastErrorText = parser.cErrorMsg catch to loEx + if type('lexer') == 'O' + lexer.CleanUp() + endif + + if type('parser') == 'O' + parser.CleanUp() + endif this.ShowExceptionError(loEx) this.lError = .t. this.LastErrorText = loEx.message finally + &&this.restoreEnvironment(loEnv) store .null. to lexer, parser release lexer, parser endtry return loJSONStr endfunc - * JSONViewer +* JSONViewer function JSONViewer as Void lparameters tcJsonStr as memo, tlStopExecution as Boolean do form frmJSONViewer with tcJsonStr, tlStopExecution @@ -141,50 +214,93 @@ define class JSONClass as session read events endif endfunc - * ====================== Old JSONFox Functions =========================== * - * . . . . . . . . . . For backward compatibility . . . . . . . . . . . . - * ======================================================================== * - && ======================================================================== && - && Function Encode - && <> please use Stringify function instead. - && ======================================================================== && - function Encode(toObj as object, tcFlags as string) as memo - local loEncode - loEncode = createobject("ObjectToJson") - return loEncode.Encode(@toObj, tcFlags) +* ====================== Old JSONFox Functions =========================== * +* . . . . . . . . . . For backward compatibility . . . . . . . . . . . . +* ======================================================================== * +&& ======================================================================== && +&& Function Encode +&& <> please use Stringify function instead. +&& ======================================================================== && + function Encode(toObj as object, tcFlags as string, tlUtf8 as Boolean, tlTrimChars as Boolean) as memo + try + &&local loEnv + &&loEnv = this.saveEnvironment() + + this.ResetError() + + local loEncode, loResult + loEncode = createobject("ObjectToJson") + loResult = loEncode.Encode(@toObj, tcFlags, tlUtf8, tlTrimChars) + catch to loEx + this.ShowExceptionError(loEx) + this.lError = .t. + this.LastErrorText = loEx.message + finally + &&this.restoreEnvironment(loEnv) + loEncode = null + release loEncode + endtry + return loResult endfunc - && ======================================================================== && - && Function decode - && <> please use Parse function instead. - && ======================================================================== && +&& ======================================================================== && +&& Function decode +&& <> please use Parse function instead. +&& ======================================================================== && function Decode(tcJsonStr as memo) as object - return this.Parse(tcJsonStr) + try + &&local loEnv, loResult + &&loEnv = this.saveEnvironment() + this.ResetError() + loResult = this.Parse(tcJsonStr) + catch to loEx + this.ShowExceptionError(loEx) + this.lError = .t. + this.LastErrorText = loEx.message + finally + &&this.restoreEnvironment(loEnv) + endtry + return loResult endfunc - && ======================================================================== && - && Function LoadFile - && <> please use Parse function instead. - && ======================================================================== && +&& ======================================================================== && +&& Function LoadFile +&& <> please use Parse function instead. +&& ======================================================================== && function LoadFile(tcJsonFile as string) as object - return this.Decode(filetostr(tcJsonFile)) + try + &&local loEnv, loResult + &&loEnv = this.saveEnvironment() + this.ResetError() + loResult = this.Decode(filetostr(tcJsonFile)) + catch to loEx + this.ShowExceptionError(loEx) + this.lError = .t. + this.LastErrorText = loEx.message + finally + &&this.restoreEnvironment(loEnv) + endtry + return loResult endfunc - * ArrayToXML +* ArrayToXML function ArrayToXML(tcArray as memo) as string - local lcOut as string, lcCursor + local lcOut as string, lcCursor&&, loEnv lcOut = '' - lcCursor = SYS(2015) + lcCursor = sys(2015) if vartype(tcArray) = 'O' try + &&loEnv = this.saveEnvironment() local objToJson objToJson = createobject("ObjectToJson") tcArray = objToJson.Encode(@tcArray) catch to loEx this.ShowExceptionError(loEx) finally + &&this.restoreEnvironment(loEnv) objToJson = .null. release objToJson endtry endif try + &&loEnv = this.saveEnvironment() this.jsonToCursor(tcArray, lcCursor, set("Datasession")) if used(lcCursor) =cursortoxml(lcCursor, 'lcOut', 1, 0, 0, '1') @@ -192,37 +308,43 @@ define class JSONClass as session catch to loEx this.ShowExceptionError(loEx) finally + &&this.restoreEnvironment(loEnv) use in (select(lcCursor)) endtry return lcOut endfunc - * XMLToJson +* XMLToJson function XMLToJson(tcXML as memo) as memo - local lcJsonXML as memo, loParser + local lcJsonXML as memo, loParser&&, loEnv lcJsonXML = '' try + &&loEnv = this.saveEnvironment() this.ResetError() =xmltocursor(tcXML, 'qXML') loParser = createobject("CursorToArray") - loParser.CurName = "qXML" + loParser.CurName = "qXML" loParser.nSessionID = set("Datasession") + loParser.ParseUTF8 = .t. + loParser.TrimChars = .t. lcJsonXML = loParser.CursorToArray() catch to loEx this.ShowExceptionError(loEx) finally + &&this.restoreEnvironment(loEnv) loParser = .null. release loParser use in (select("qXML")) endtry return lcJsonXML endfunc - * CursorToJSON +* CursorToJSON function CursorToJSON as memo - lparameters tcCursor as string, tbCurrentRow as Boolean, tnDataSession as integer, tlJustArray as Boolean - local lcJsonXML as memo, loParser, lcCursor + lparameters tcCursor as string, tbCurrentRow as Boolean, tnDataSession as integer, tlJustArray as Boolean, tlParseUtf8 as Boolean, tlTrimChars as Boolean + local lcJsonXML as memo, loParser, lcCursor&&, loEnv lcJsonXML = '' - lcCursor = SYS(2015) + lcCursor = sys(2015) try + &&loEnv = this.saveEnvironment() this.ResetError() tcCursor = evl(tcCursor, alias()) tnDataSession = evl(tnDataSession, set("Datasession")) @@ -236,10 +358,17 @@ define class JSONClass as session loParser = createobject("CursorToArray") loParser.CurName = lcCursor loParser.nSessionID = tnDataSession +&& IRODG 07/10/2023 Inicio + loParser.ParseUTF8 = tlParseUtf8 +&& IRODG 07/10/2023 Fin +&& IRODG 27/10/2023 Inicio + loParser.TrimChars = tlTrimChars +&& IRODG 27/10/2023 Fin lcJsonXML = loParser.CursorToArray() catch to loEx this.ShowExceptionError(loEx) finally + &&this.restoreEnvironment(loEnv) loParser = .null. release loParser use in (select(lcCursor)) @@ -247,11 +376,75 @@ define class JSONClass as session lcOutput = iif(tlJustArray, lcJsonXML, '{"' + lower(alltrim(tcCursor)) + '":' + lcJsonXML + '}') return lcOutput endfunc - * JSONToCursor + +* CursorToJSONObject + function CursorToJSONObject(tcCursor as string, tbCurrentRow as Boolean, tnDataSession as integer) as object + local loParser, lcCursor, lnRecno, loResult as Variant&&, loEnv + lcCursor = sys(2015) + try + &&loEnv = this.saveEnvironment() + this.ResetError() + tcCursor = evl(tcCursor, alias()) + tnDataSession = evl(tnDataSession, set("Datasession")) + set datasession to tnDataSession + if tbCurrentRow + lnRecno = recno(tcCursor) + select * from (tcCursor) where recno() = lnRecno into cursor (lcCursor) + else + select * from (tcCursor) into cursor (lcCursor) + endif + loParser = createobject("CursorToJsonObject") + loParser.CurName = lcCursor + loParser.nSessionID = tnDataSession + loResult = loParser.CursorToJSONObject() + catch to loEx + this.ShowExceptionError(loEx) + finally + &&this.restoreEnvironment(loEnv) + loParser = .null. + release loParser + use in (select(lcCursor)) + endtry + + if type('loResult', 1) == 'A' + local i + for i = 1 to alen(loResult, 1) + dimension this.aCustomArray[i] + this.aCustomArray[i] = loResult[i] + endfor + return @this.aCustomArray + else + return loResult + endif + endfunc + + function MasterDetailToJSON(tcMaster as string, tcDetail as string, tcExpr as string, tcDetailAttribute as string, tnSessionID as integer) + local loClass, loResult, lcResult&&, loEnv + try + &&loEnv = this.saveEnvironment() + this.ResetError() + tnSessionID = evl(tnSessionID, set("Datasession")) + set datasession to tnSessionID + loClass = createobject("CursorToJsonObject") + loResult = loClass.MasterDetailToJSON(tcMaster, tcDetail, tcExpr, tcDetailAttribute, tnSessionID) + catch to loEx + this.ShowExceptionError(loEx) + finally + &&this.restoreEnvironment(loEnv) + loClass = .null. + release loClass + endtry +*lcResult = this.stringify(@loResult) + lcResult = this.Encode(@loResult, "", .t., .t.) + return lcResult + endfunc + +* JSONToCursor function jsonToCursor(tcJsonStr as memo, tcCursor as string, tnDataSession as integer) as Void try - local lexer, parser + local lexer, parser, loEnv this.ResetError() + loEnv = this.saveEnvironment() if !empty(tcCursor) tnDataSession = evl(tnDataSession, set("Datasession")) lexer = createobject("Tokenizer", tcJsonStr) @@ -265,18 +458,27 @@ define class JSONClass as session endif endif catch to loEx + if type('lexer') == 'O' + lexer.CleanUp() + endif + + if type('parser') == 'O' + parser.CleanUp() + endif this.ShowExceptionError(loEx) finally + this.restoreEnvironment(loEnv) store .null. to lexer, parser release lexer, parser endtry endfunc - * CursorStructure +* CursorStructure function CursorStructure - lparameters tcCursor as string, tnDataSession as integer, tlCopyExtended as Boolean, tlJustArray As Boolean - local lcOutput as memo + lparameters tcCursor as string, tnDataSession as integer, tlCopyExtended as Boolean, tlJustArray as Boolean + local lcOutput as memo, loEnv lcOutput = '' try + loEnv = this.saveEnvironment() this.ResetError() loStructureToJSON = createobject("StructureToJSON") tcCursor = evl(tcCursor, alias()) @@ -288,31 +490,39 @@ define class JSONClass as session lcOutput = loStructureToJSON.StructureToJSON() catch to loEx this.ShowExceptionError(loEx) + finally + this.restoreEnvironment(loEnv) endtry return lcOutput endfunc - * tokenize - function dumpTokens +* tokenize + function dumpTokens2 lparameters tcJsonStr as memo + &&local loEnv try this.ResetError() - local loLexer, laTokens, lcTokens as memo, i + &&loEnv = this.saveEnvironment() + local loLexer, laTokenCollection, lcTokens as memo, i loLexer = createobject("Tokenizer", tcJsonStr) - laTokens = loLexer.scanTokens() + laTokenCollection = loLexer.scanTokens() lcTokens = '' - For i = 1 to Alen(laTokens) - lcTokens = lcTokens + loLexer.tokenStr(laTokens[i]) + CHR(13) + CHR(10) + for i = 1 to alen(laTokenCollection.Tokens) + lcTokens = lcTokens + loLexer.tokenStr(laTokenCollection.Tokens[i]) + chr(13) + chr(10) endfor catch to loEx + if type('lexer') == 'O' + lexer.CleanUp() + endif this.ShowExceptionError(loEx) finally + &&this.restoreEnvironment(loEnv) store .null. to lexer, parser release lexer, parser endtry _cliptext = lcTokens return lcTokens endfunc - * LastErrorText_Assign +* LastErrorText_Assign function LastErrorText_Assign lparameters vNewVal with this @@ -322,7 +532,7 @@ define class JSONClass as session endif endwith endfunc - * ShowExceptionError +* ShowExceptionError function ShowExceptionError(toEx as exception) as Void with this .lError = .t. @@ -336,11 +546,11 @@ define class JSONClass as session .LastErrorText = toEx.message endwith endfunc - * ResetError +* ResetError hidden function ResetError as Void this.lError = .f. endfunc - * Destroy +* Destroy function destroy try if this.lTablePrompt @@ -349,7 +559,7 @@ define class JSONClass as session endif catch endtry - && >>>>>>> IRODG 12/28/21 +&& >>>>>>> IRODG 12/28/21 try removeproperty(_screen, 'json') catch @@ -366,6 +576,30 @@ define class JSONClass as session removeproperty(_screen, 'toml') catch endtry - && <<<<<<< IRODG 12/28/21 +&& <<<<<<< IRODG 12/28/21 endfunc -enddefine + + protected function saveEnvironment + try + local loEnv + loEnv = createobject("Collection") + + loEnv.add(set("POINT"), "point") + loEnv.add(set("SEPARATOR"), "separator") + + set point to '.' + set separator to ',' + catch + endtry + + return loEnv + endproc + + protected procedure restoreEnvironment(toEnv as collection) + try + set point to toEnv("point") + set separator to toEnv("separator") + catch + endtry + endproc +enddefine \ No newline at end of file diff --git a/src/jsonfox.h b/src/jsonfox.h index bd34f50..9175648 100644 --- a/src/jsonfox.h +++ b/src/jsonfox.h @@ -23,4 +23,5 @@ #Define CR Chr(13) #Define LF Chr(10) #Define CRLF CR + LF -#Define T_TAB Chr(9) \ No newline at end of file +#Define T_TAB Chr(9) +#Define INTEGER_MAX_CAPACITY 2147483647 \ No newline at end of file diff --git a/src/jsonstringify.prg b/src/jsonstringify.prg index d57a609..556c4f8 100644 --- a/src/jsonstringify.prg +++ b/src/jsonstringify.prg @@ -10,24 +10,29 @@ define class JSONStringify as custom ParseUtf8 = .f. + TrimChars = .f. - Dimension tokens[1] Hidden current Hidden previous Hidden peek + hidden tokenCollection function init(toScanner) - Local laTokens - laTokens = toScanner.scanTokens() - =Acopy(laTokens, this.tokens) + this.tokenCollection = toScanner.scanTokens() this.current = 1 endfunc * Stringify function Stringify as memo - lparameters tlParseUtf8 + lparameters tlParseUtf8, tlTrimChars + local lcFormatedJson this.ParseUtf8 = tlParseUtf8 - return this.value(0) + this.TrimChars = tlTrimChars + + lcFormatedJson = this.value(0) + this.CleanUp() + + return lcFormatedJson endfunc && ======================================================================== && && Function Object @@ -62,9 +67,10 @@ define class JSONStringify as custom lparameters tnSpaceIdent as integer local lcProp as string this.consume(T_STRING, "Expect right key element") - lcProp = this.previous.value - this.consume(T_COLON, "Expect ':' after key element.") - return '"' + lcProp + '": ' + this.value(tnSpaceIdent) + lcProp = _screen.JSONUtils.GetString(this.previous.value, this.ParseUtf8) + this.consume(T_COLON, "Expect ':' after key element.") + *return '"' + lcProp + '": ' + this.value(tnSpaceIdent) + return lcProp + ': ' + this.value(tnSpaceIdent) endfunc && ======================================================================== && && Function Value @@ -73,7 +79,7 @@ define class JSONStringify as custom hidden function value(tnSpaceBlock) do case case this.match(T_STRING) - return _screen.JSONUtils.GetString(this.previous.value, this.ParseUtf8) + return _screen.JSONUtils.GetString(Iif(this.TrimChars, Alltrim(this.previous.value), this.previous.value), this.ParseUtf8) case this.match(T_NUMBER) return this.previous.value @@ -156,7 +162,7 @@ define class JSONStringify as custom If !this.isAtEnd() this.current = this.current + 1 EndIf - Return this.tokens[this.current-1] + Return this.tokenCollection.tokens[this.current-1] endfunc Hidden Function isAtEnd @@ -164,11 +170,20 @@ define class JSONStringify as custom endfunc Hidden Function peek_access - Return this.tokens[this.current] + Return this.tokenCollection.tokens[this.current] endfunc Hidden Function previous_access - Return this.tokens[this.current-1] + Return this.tokenCollection.tokens[this.current-1] EndFunc + function CleanUp + with this + .TokenCollection = .null. + + .current = 0 + .previous = .null. + .peek = 0 + endwith + endfunc enddefine diff --git a/src/jsontortf.prg b/src/jsontortf.prg index f404cb0..fcc86d4 100644 --- a/src/jsontortf.prg +++ b/src/jsontortf.prg @@ -13,20 +13,17 @@ define class JSONToRTF as custom lError = .f. cErrorMsg = "" - - Dimension tokens[1] Hidden current Hidden previous Hidden peek + hidden tokenCollection && ======================================================================== && && Function Init && ======================================================================== && function init(toScanner) this.lError = .f. - Local laTokens - laTokens = toScanner.scanTokens() - =Acopy(laTokens, this.tokens) + this.tokenCollection = toScanner.scanTokens() this.current = 1 endfunc @@ -176,7 +173,7 @@ define class JSONToRTF as custom If !this.isAtEnd() this.current = this.current + 1 EndIf - Return this.tokens[this.current-1] + Return this.tokenCollection.tokens[this.current-1] endfunc Hidden Function isAtEnd @@ -184,10 +181,20 @@ define class JSONToRTF as custom endfunc Hidden Function peek_access - Return this.tokens[this.current] + Return this.tokenCollection.tokens[this.current] endfunc Hidden Function previous_access - Return this.tokens[this.current-1] + Return this.tokenCollection.tokens[this.current-1] EndFunc + + function CleanUp + with this + .TokenCollection = .null. + + .current = 0 + .previous = .null. + .peek = 0 + endwith + endfunc enddefine diff --git a/src/jsonutils.prg b/src/jsonutils.prg index 5b81cc1..211d528 100644 --- a/src/jsonutils.prg +++ b/src/jsonutils.prg @@ -4,34 +4,55 @@ && JSON Utilities && ======================================================================== && define class jsonutils as custom - - Dimension aPattern[5, 2] - - Function init + + dimension aPattern[8, 2] + + function init +&& Match a date format in the following pattern +&& "YYYY-MM-DD" this.aPattern[1,1] = "^\d\d\d\d-(0?[1-9]|1[0-2])-(0?[1-9]|[12][0-9]|3[01])$" this.aPattern[1,2] = .f. - + +&& Match a date and time format in the following pattern +&& "YYYY-MM-DD HH:MM:SS" this.aPattern[2,1] = "^\d\d\d\d-(0?[1-9]|1[0-2])-(0?[1-9]|[12][0-9]|3[01]) (00|0?[0-9]|1[0-9]|2[0-3]):([0-9]|[0-5][0-9]):([0-9]|[0-5][0-9])$" this.aPattern[2,2] = .f. - + +&& Match ISO 8601 date and time formats that include a time zone offset +&& "YYYY-MM-DDTHH:MM:SSZ" OR "YYYY-MM-DDTHH:MM:SS+HH:MM" OR "YYYY-MM-DDTHH:MM:SS-HH:MM" this.aPattern[3,1] = "^(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})(\:(\d{2}))?(Z|[+-](\d{2})\:(\d{2}))?$" this.aPattern[3,2] = .f. - + +&& Match a date and time format in ISO 8601 combined with a single-character time zone identifier +&& "YYYY-MM-DDTHH:MM(:SS)?.SSS(W)" this.aPattern[4,1] = "^(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})(\:(\d{2}))?[.](\d{3})(\w{1})$" this.aPattern[4,2] = .f. +&& "DD/MM/YYYY" OR "DD-MM-YYYY" this.aPattern[5,1] = "^([0-2][0-9]|(3)[0-1])[\/-](((0)[0-9])|((1)[0-2]))[\/-]\d{4}$" this.aPattern[5,2] = .t. +&& "DD/MM/YYYY HH:MM:SS" or "DD-MM-YYYY HH:MM:SS" + this.aPattern[06,1] = "^([0-2][0-9]|(3)[0-1])[\/-](((0)[0-9])|((1)[0-2]))[\/-]\d{4} (00|0?[0-9]|1[0-9]|2[0-3]):([0-9]|[0-5][0-9]):([0-9]|[0-5][0-9])$" + this.aPattern[06,2] = .t. + +&& "DD/MM/YY" or "DD-MM-YY" + this.aPattern[07,1] = "^([0-2][0-9]|(3)[0-1])[\/-](((0)[0-9])|((1)[0-2]))[\/-]\d{2}$" + this.aPattern[07,2] = .t. + +&& "DD/MM/YY HH:MM:SS" or "DD-MM-YY HH:MM:SS" + this.aPattern[08,1] = "^([0-2][0-9]|(3)[0-1])[\/-](((0)[0-9])|((1)[0-2]))[\/-]\d{2} (00|0?[0-9]|1[0-9]|2[0-3]):([0-9]|[0-5][0-9]):([0-9]|[0-5][0-9])$" + this.aPattern[08,2] = .t. + _screen.oRegEx.global = .t. - EndFunc + endfunc - && ======================================================================== && - && Function GetValue - && ======================================================================== && - function getvalue as string - lparameters tcvalue as string, tctype as character +&& ======================================================================== && +&& Function GetValue +&& ======================================================================== && + function getValue as string + lparameters tcvalue as string, tctype as character, tlParseUTF8 as Boolean, tlTrimChars as Boolean do case case tctype $ "CDTBGMQVWX" do case @@ -39,48 +60,59 @@ define class jsonutils as custom tcvalue = '"' + strtran(dtoc(tcvalue), '.', '-') + '"' case tctype == 'T' tcvalue = '"' + strtran(ttoc(tcvalue), '.', '-') + '"' - Case tctype == 'X' + case tctype == 'X' tcvalue = "null" - Otherwise - tcvalue = this.getstring(tcvalue) + otherwise + tcvalue = this.getString(iif(tlTrimChars, alltrim(tcvalue), tcvalue), tlParseUTF8) endcase - tcvalue = alltrim(tcvalue) case tctype $ "YFIN" - tcvalue = strtran(transform(tcvalue), ',', '.') + if this.HasDecimals(tcvalue) + tcvalue = strtran(alltrim(transform(tcvalue, "@T")), ',', '.') + else + tcvalue = strtran(alltrim(transform(tcvalue)), ',', '.') + endif case tctype == 'L' tcvalue = iif(tcvalue, "true", "false") endcase return tcvalue endfunc - && ======================================================================== && - && Function CheckString - && Check the string content in case it is a date or datetime. - && String itself or string date / datetime format. - && ======================================================================== && + + function HasDecimals(tnValue, tnTolerance) + if pcount() < 2 + tnTolerance = 0.0000001 + endif + return abs(tnValue - int(tnValue)) > tnTolerance + endfunc + +&& ======================================================================== && +&& Function CheckString +&& Check the string content in case it is a date or datetime. +&& String itself or string date / datetime format. +&& ======================================================================== && function CheckString(tcString) - If !IsDigit(Left(tcString, 1)) and !IsDigit(Right(tcString, 1)) - Return tcString - EndIf - * We try to identify a date format - Local i - For i = 1 to Alen(this.aPattern, 1) + if !isdigit(left(tcString, 1)) and !isdigit(right(tcString, 1)) + return tcString + endif +* We try to identify a date format + local i + for i = 1 to alen(this.aPattern, 1) _screen.oRegEx.pattern = this.aPattern[i, 1] if _screen.oRegEx.Test(tcString) - return this.formatDate(tcString, this.aPattern[i, 2]) + return evl(this.formatDate(tcString, this.aPattern[i, 2]), tcString) endif - EndFor - * It is a normal String + endfor +* It is a normal String return tcString endfunc - && ======================================================================== && - && Function FormatDate - && return a valid date or datetime date type. - && ======================================================================== && +&& ======================================================================== && +&& Function FormatDate +&& return a valid date or datetime date type. +&& ======================================================================== && function formatDate as variant lparameters tcDate as string, tlUseDMY as Boolean local lDate lDate = .null. - && IRODG 20210313 ISSUE # 14 +&& IRODG 20210313 ISSUE # 14 do case case 'T' $ tcDate && JavaScript or ISO 8601 format. do case @@ -99,10 +131,17 @@ define class jsonutils as custom finally set date &setDateAct endtry - case occurs(':', tcDate) >= 2 && VFP Date Time Format. 'YYYY-mm-dd HH:mm:ss' + case occurs(':', tcDate) >= 2 && VFP Date Time Format. 'YYYY-mm-dd HH:mm:ss' and also 'dd-mm-yyyy hh:mm:ss' try setDateAct = set('Date') - set date ymd +* set date ymd +&& (DCA) - 12/09/2023 - Also verify if the DateTime is DMY Format + if !tlUseDMY + set date ymd + else + set date dmy + endif + lDate = ctot(tcDate) catch lDate = {//::} @@ -125,62 +164,70 @@ define class jsonutils as custom endtry endcase return lDate - && IRODG 20210313 ISSUE # 14 +&& IRODG 20210313 ISSUE # 14 endfunc - && ======================================================================== && - && Function GetString - && ======================================================================== && - function getstring as string - lparameters tcString as string, tlParseUtf8 as Boolean - tcString = allt(tcString) - tcString = strtran(tcString, '\', '\\' ) +&& ======================================================================== && +&& Function GetString +&& ======================================================================== && + function getString as string + lparameters tcString as string, tlParseUTF8 as Boolean +&& IRODG 08/08/2023 Inicio +*tcString = Alltrim(tcString) +&& IRODG 08/08/2023 Fin + tcString = strtran(tcString, '\', '\\' ) tcString = strtran(tcString, chr(9), '\t' ) tcString = strtran(tcString, chr(10), '\n' ) tcString = strtran(tcString, chr(13), '\r' ) + if left(alltrim(tcString), 1) == '"' and right(alltrim(tcString),1) == '"' + tcString = substr(tcString, 2, len(tcString)-2) + endif tcString = strtran(tcString, '"', '\"' ) - if tlParseUtf8 - tcString = StrTran(tcString,"&","\u0026") - tcString = StrTran(tcString,"+","\u002b") - tcString = StrTran(tcString,"-","\u002d") - tcString = StrTran(tcString,"#","\u0023") - tcString = StrTran(tcString,"%","\u0025") - tcString = StrTran(tcString,"","\u00b2") - tcString = StrTran(tcString,'','\u00e0') - tcString = StrTran(tcString,'','\u00e1') - tcString = StrTran(tcString,'','\u00e8') - tcString = StrTran(tcString,'','\u00e9') - tcString = StrTran(tcString,'','\u00ec') - tcString = StrTran(tcString,'','\u00ed') - tcString = StrTran(tcString,'','\u00f2') - tcString = StrTran(tcString,'','\u00f3') - tcString = StrTran(tcString,'','\u00f9') - tcString = StrTran(tcString,'','\u00fa') - tcString = StrTran(tcString,'','\u00fc') - tcString = StrTran(tcString,'','\u00c0') - tcString = StrTran(tcString,'','\u00c1') - tcString = StrTran(tcString,'','\u00c8') - tcString = StrTran(tcString,'','\u00c9') - tcString = StrTran(tcString,'','\u00cc') - tcString = StrTran(tcString,'','\u00cd') - tcString = StrTran(tcString,'','\u00d2') - tcString = StrTran(tcString,'','\u00d3') - tcString = StrTran(tcString,'','\u00d9') - tcString = StrTran(tcString,'','\u00da') - tcString = StrTran(tcString,'','\u00dc') - tcString = StrTran(tcString,'','\u00f1') - tcString = StrTran(tcString,'','\u00d1') - tcString = StrTran(tcString,'','\u00a9') - tcString = StrTran(tcString,'','\u00ae') - tcString = StrTran(tcString,'','\u00e7') + if tlParseUTF8 + tcString = strtran(tcString,"&","\u0026") + tcString = strtran(tcString,"+","\u002b") + tcString = strtran(tcString,"-","\u002d") + tcString = strtran(tcString,"#","\u0023") + tcString = strtran(tcString,"%","\u0025") + tcString = strtran(tcString,"","\u00b2") + tcString = strtran(tcString,'','\u00e0') + tcString = strtran(tcString,'','\u00e1') + tcString = strtran(tcString,'','\u00e8') + tcString = strtran(tcString,'','\u00e9') + tcString = strtran(tcString,'','\u00ec') + tcString = strtran(tcString,'','\u00ed') + tcString = strtran(tcString,'','\u00f2') + tcString = strtran(tcString,'','\u00f3') + tcString = strtran(tcString,'','\u00f9') + tcString = strtran(tcString,'','\u00fa') + tcString = strtran(tcString,'','\u00fc') + tcString = strtran(tcString,'','\u00c0') + tcString = strtran(tcString,'','\u00c1') + tcString = strtran(tcString,'','\u00c8') + tcString = strtran(tcString,'','\u00c9') + tcString = strtran(tcString,'','\u00cc') + tcString = strtran(tcString,'','\u00cd') + tcString = strtran(tcString,'','\u00d2') + tcString = strtran(tcString,'','\u00d3') + tcString = strtran(tcString,'','\u00d9') + tcString = strtran(tcString,'','\u00da') + tcString = strtran(tcString,'','\u00dc') + tcString = strtran(tcString,'','\u00f1') + tcString = strtran(tcString,'','\u00d1') + tcString = strtran(tcString,'','\u00a9') + tcString = strtran(tcString,'','\u00ae') + tcString = strtran(tcString,'','\u00e7') + tcString = strtran(tcString,'','\u00ba') endif - - return '"' +tcString + '"' + if left(alltrim(tcString), 1) != '"' and right(alltrim(tcString),1) != '"' + return '"'+tcString+'"' + endif + return tcString endfunc - && ======================================================================== && - && Function CheckProp - && Check the object property name for invalid format (replace space with '_') - && ======================================================================== && +&& ======================================================================== && +&& Function CheckProp +&& Check the object property name for invalid format (replace space with '_') +&& ======================================================================== && function checkprop(tcprop as string) as string local lcfinalprop, i, lcchar lcfinalprop = '' @@ -193,48 +240,48 @@ define class jsonutils as custom endif endfor return alltrim(lcfinalprop) - EndFunc - - Function tokenTypeToStr(tnType) + endfunc + + function tokenTypeToStr(tnType) do case case tnType = 0 - Return 'EOF' + return 'EOF' case tnType = 1 - Return 'LBRACE' + return 'LBRACE' case tnType = 2 - Return 'RBRACE' + return 'RBRACE' case tnType = 3 - Return 'LBRACKET' + return 'LBRACKET' case tnType = 4 - Return 'RBRACKET' + return 'RBRACKET' case tnType = 5 - Return 'COMMA' + return 'COMMA' case tnType = 6 - Return 'COLON' + return 'COLON' case tnType = 7 - Return 'TRUE' + return 'TRUE' case tnType = 8 - Return 'FALSE' + return 'FALSE' case tnType = 9 - Return 'NULL' + return 'NULL' case tnType = 10 - Return 'NUMBER' + return 'NUMBER' case tnType = 11 - Return 'KEY' + return 'KEY' case tnType = 12 - Return 'STRING' + return 'STRING' case tnType = 13 - Return 'LINE' + return 'LINE' case tnType = 14 - Return 'INTEGER' + return 'INTEGER' case tnType = 15 - Return 'FLOAT' + return 'FLOAT' case tnType = 16 - Return 'VALUE' + return 'VALUE' case tnType = 17 - Return 'EOF' + return 'EOF' case tnType = 18 return 'BOOLEAN' - ENDCASE - EndFunc + endcase + endfunc enddefine diff --git a/src/loader.prg b/src/loader.prg index ff8e8f8..443b54b 100644 --- a/src/loader.prg +++ b/src/loader.prg @@ -4,10 +4,12 @@ Set Procedure To "src\JSONClass" Additive Set Procedure To "src\Tokenizer" Additive Set Procedure To "src\NetScanner" Additive +Set Procedure To "src\jscriptscanner" Additive Set Procedure To "src\Parser" Additive Set Procedure To "src\JSONUtils" Additive Set Procedure To "src\ArrayToCursor" Additive Set Procedure To "src\CursorToArray" Additive +Set Procedure To "src\CursorToJsonObject" Additive Set Procedure To "src\JSONStringify" Additive Set Procedure To "src\ObjectToJSON" Additive Set Procedure To "src\JSONToRTF" Additive @@ -41,6 +43,4 @@ Endif If Type("_Screen.Toml") != "U" =Removeproperty(_Screen, 'Toml') Endif -=AddProperty(_Screen, "Toml", Createobject("TomlClass")) - -Return \ No newline at end of file +=AddProperty(_Screen, "Toml", Createobject("TomlClass")) \ No newline at end of file diff --git a/src/objecttojson.prg b/src/objecttojson.prg index 5db27cb..767418b 100644 --- a/src/objecttojson.prg +++ b/src/objecttojson.prg @@ -6,6 +6,9 @@ define class ObjectToJSON as session cDateAct = '' nOrden = 0 cFlags = '' + parseUTF8 = .f. + TrimChars = .f. + * Function Init function init this.lCentury = set("Century") == "OFF" @@ -14,8 +17,9 @@ define class ObjectToJSON as session set date ansi mvcount = 60000 endfunc + * Encode - function Encode(toRefObj, tcFlags) + function Encode(toRefObj, tcFlags, tlParseUTF8, tlTrimChars) lPassByRef = .t. try external array toRefObj @@ -23,12 +27,19 @@ define class ObjectToJSON as session lPassByRef = .f. endtry this.cFlags = evl(tcFlags, ALL_MEMBERS) + this.parseUTF8 = tlParseUTF8 + this.TrimChars = tlTrimChars if lPassByRef return this.AnyToJson(@toRefObj) else return this.AnyToJson(toRefObj) endif endfunc + + function EncodeFromSchema(toRefObj, tcSchema, tlParseUTF8, tlTrimChars) + + endfunc + * AnyToJson function AnyToJson as memo lparameters tValue as Variant @@ -37,7 +48,6 @@ define class ObjectToJSON as session catch endtry do case - *case type("Alen(tValue, 1)") = "N" case type("tValue", 1) = 'A' local k, j, lcArray if alen(tValue, 2) == 0 @@ -46,9 +56,9 @@ define class ObjectToJSON as session for k = 1 to alen(tValue) lcArray = lcArray + iif(len(lcArray) > 1, ',', '') try - *local array aLista(alen(tValue[k])) - =acopy(tValue[k], aLista) - lcArray = lcArray + this.AnyToJson(@aLista) + local array laLista[1] + =acopy(tValue[k], laLista) + lcArray = lcArray + this.AnyToJson(@laLista) catch lcArray = lcArray + this.AnyToJson(tValue[k]) endtry @@ -67,8 +77,9 @@ define class ObjectToJSON as session lcArray = lcArray + ',' endif try - =acopy(tValue[k, j], aLista) - lcArray = lcArray + this.AnyToJson(@aLista) + local array laLista[1] + =acopy(tValue[k, j], laLista) + lcArray = lcArray + this.AnyToJson(@laLista) catch lcArray = lcArray + this.AnyToJson(tValue[k, j]) endtry @@ -81,25 +92,63 @@ define class ObjectToJSON as session return lcArray case vartype(tValue) = 'O' - local j, lcJSONStr, lnTot, i + local j, lcJSONStr, lnTot, i, lcProp, lcOriginalName local array gaMembers(1) lcJSONStr = '{' lnTot = amembers(gaMembers, tValue, 0, this.cFlags) + + local array laPropsToProcess[1] + local lnPropCount, lnIdx + lnPropCount = 0 + + * primer paso: identificar y clasificar las propiedades for j=1 to lnTot lcProp = lower(alltrim(gaMembers[j])) - lcJSONStr = lcJSONStr + iif(len(lcJSONStr) > 1, ',', '') + '"' + lcProp + '":' - try - *local array aCopia(alen(tValue. &gaMembers[j])) - =acopy(tValue. &gaMembers[j], aCopia) - lcJSONStr = lcJSONStr + this.AnyToJson(@aCopia) - catch - try - lcJSONStr = lcJSONStr + this.AnyToJson(tValue. &gaMembers[j]) - catch - lcJSONStr = lcJSONStr + "{}" - endtry - endtry + * Ignoramos propiedades especiales de array + if left(lower(lcProp), 14) == "_specialarray_" + loop + endif + + lnPropCount = lnPropCount + 1 + dimension laPropsToProcess[lnPropCount,2] + laPropsToProcess[lnPropCount, 1] = lcProp + if right(lcProp, 6) == "_array" and type("tValue._specialArray_" + lcProp) == "C" + laPropsToProcess[lnPropCount, 2] = "special_array" + else + laPropsToProcess[lnPropCount, 2] = "normal" + endif + next + + * segundo paso: procesar las propiedades filtradas + for lnIdx=1 to lnPropCount + lcProp = laPropsToProcess[lnIdx, 1] + + if laPropsToProcess[lnIdx, 2] == "special_array" + lcOriginalName = evaluate("tValue._specialArray_" + lcProp) + lcJSONStr = lcJSONStr + iif(len(lcJSONStr) > 1, ',', '') + '"' + lcOriginalName + '":' + try + local array laLista[1] + =acopy(tValue. &lcProp, laLista) + lcJSONStr = lcJSONStr + this.AnyToJson(@laLista) + catch + lcJSONStr = lcJSONStr + "[]" + endtry + else + * Es una propiedad normal + lcJSONStr = lcJSONStr + iif(len(lcJSONStr) > 1, ',', '') + '"' + lcProp + '":' + try + local array laLista[1] + =acopy(tValue. &lcProp, laLista) + lcJSONStr = lcJSONStr + this.AnyToJson(@laLista) + catch + try + lcJSONStr = lcJSONStr + this.AnyToJson(tValue. &lcProp) + catch + lcJSONStr = lcJSONStr + "{}" + endtry + endtry + endif endfor *//> Collection based class object support @@ -121,7 +170,7 @@ define class ObjectToJSON as session lcJSONStr = lcJSONStr + '}' return lcJSONStr otherwise - return _screen.JSONUtils.GetValue(tValue, vartype(tValue)) + return _screen.JSONUtils.GetValue(tValue, vartype(tValue), this.parseUTF8, this.TrimChars) endcase endfunc * Destroy diff --git a/src/parser.prg b/src/parser.prg index f03e640..363b30d 100644 --- a/src/parser.prg +++ b/src/parser.prg @@ -8,24 +8,27 @@ && array = '[' value | { ',' value } ']' && ======================================================================== && define class Parser as custom - Dimension tokens[1] Hidden current Hidden previous Hidden peek + hidden problematicFields + hidden tokenCollection function init(toScanner) - With this - Local laTokens - laTokens = toScanner.scanTokens() - =Acopy(laTokens, .tokens) - .current = 1 - endwith + this.tokenCollection = toScanner.scanTokens() + this.current = 1 + + this.problematicFields = createobject("Collection") + this.problematicFields.Add("messages", "messages") + this.problematicFields.Add("update", "update") endfunc - function Parse - With this - Return .value() - endwith + function Parse + local loParsedObject + loParsedObject = this.value() + this.CleanUp() + + return loParsedObject endfunc && ======================================================================== && && Function Object @@ -33,62 +36,65 @@ define class Parser as custom && kvp = KEY ':' value && ======================================================================== && hidden function object as object - With this - local loObj, loPair, lcMacro - loObj = createobject('Empty') + local loObj, loPair, lcMacro + loObj = createobject('Empty') + if !this.check(T_RBRACE) + loPair = this.kvp() + this.addKeyValuePair(@loObj, @loPair) - if !.check(T_RBRACE) - loPair = .kvp() - .addKeyValuePair(@loObj, @loPair) - - do while .match(T_COMMA) - loPair = .kvp() - .addKeyValuePair(@loObj, @loPair) - enddo - endif - .consume(T_RBRACE, "Expect '}' after JSON body.") - - return loObj - endwith + do while this.match(T_COMMA) + loPair = this.kvp() + this.addKeyValuePair(@loObj, @loPair) + enddo + endif + this.consume(T_RBRACE, "Expect '}' after JSON body.") + return loObj endfunc && ======================================================================== && && Function Kvp && EBNF -> kvp = KEY ':' value && ======================================================================== && hidden function kvp(toObj) - With this - local loPair, lvValue - - loPair = CreateObject('Empty') - =AddProperty(loPair, 'key', '') - - .consume(T_STRING, "Expect key name") - - loPair.key = _screen.jsonUtils.CheckProp(.previous.value) - - .consume(T_COLON, "Expect ':' after key element.") - - lvValue = .value() - If Type('lvValue', 1) != 'A' - =AddProperty(loPair, 'value', lvValue) - Else - =AddProperty(loPair, 'value[1]', .Null.) - Acopy(lvValue, loPair.value) - endif - - Return loPair - EndWith + local loPair, lvValue + + loPair = CreateObject('Empty') + =AddProperty(loPair, 'key', '') + + this.consume(T_STRING, "Expect key name") + loPair.key = _screen.jsonUtils.CheckProp(this.previous.value) + this.consume(T_COLON, "Expect ':' after key element.") + lvValue = this.value() + If Type('lvValue', 1) != 'A' + =AddProperty(loPair, 'value', lvValue) + Else + =AddProperty(loPair, 'value[1]', .Null.) + Acopy(lvValue, loPair.value) + endif + Return loPair EndFunc Hidden function addKeyValuePair(toObject, toPair) If Type('toPair.value', 1) != 'A' =AddProperty(toObject, toPair.key, toPair.value) - Else - Local lcMacro - lcMacro = "AddProperty(toObject, '" + toPair.key + "[1]', .Null.)" - &lcMacro - lcMacro = "Acopy(toPair.value, toObject." + toPair.key + ")" - &lcMacro + else + local lcMacro + if this.problematicFields.GetKey(toPair.key) > 0 + local lcArrayName + lcArrayName = toPair.key + "_array" + lcMacro = "AddProperty(toObject, '" + lcArrayName + "[1]', .null.)" + &lcMacro + + lcMacro = "Acopy(toPair.value, toObject." + lcArrayName + ")" + &lcMacro + + =addproperty(toObject, "_specialArray_" + lcArrayName, toPair.key) + else + Local lcMacro + lcMacro = "AddProperty(toObject, '" + toPair.key + "[1]', .Null.)" + &lcMacro + lcMacro = "Acopy(toPair.value, toObject." + toPair.key + ")" + &lcMacro + endif EndIf EndFunc @@ -97,114 +103,104 @@ define class Parser as custom && EBNF -> value = STRING | NUMBER | BOOLEAN | array | object | NULL && ======================================================================== && hidden function value - With this - do case - case .match(T_STRING) - return _screen.jsonUtils.CheckString(.previous.value) - - case .match(T_NUMBER) - Local lcValue, lcPoint - lcValue = .previous.value - lcPoint = Set("Point") - - If lcPoint != '.' - lcValue = Strtran(lcValue, '.', lcPoint) - EndIf - return iif(at(lcPoint, lcValue) > 0, Val(lcValue), int(Val(lcValue))) - - case .match(T_BOOLEAN) - return (.previous.value == 'true') + do case + case this.match(T_STRING) + return _screen.jsonUtils.CheckString(this.previous.value) + + case this.match(T_NUMBER) + Local lcValue, lcPoint + lcValue = this.previous.value + lcPoint = Set("Point") + + If lcPoint != '.' + lcValue = Strtran(lcValue, '.', lcPoint) + EndIf + return iif(at(lcPoint, lcValue) > 0, Val(lcValue), int(Val(lcValue))) + + case this.match(T_BOOLEAN) + return (this.previous.value == 'true') - case .match(T_LBRACE) - return .object() + case this.match(T_LBRACE) + return this.object() - case .match(T_LBRACKET) - return @.array() - - case .match(T_NULL) - return .null. - otherwise - error "Parser Error: Unknown token value: '" + _screen.jsonUtils.tokenTypeToStr(.peek.type) + "'" - EndCase - EndWith + case this.match(T_LBRACKET) + return @this.array() + + case this.match(T_NULL) + return .null. + otherwise + error "Parser Error: Unknown token value: '" + _screen.jsonUtils.tokenTypeToStr(this.peek.type) + "'" + EndCase endfunc && ======================================================================== && && Function Array && EBNF -> array = '[' value | { ',' value } ']' && ======================================================================== && hidden function array - With this - local laArray - laArray = createobject("TParserInternalArray") - If !.check(T_RBRACKET) - laArray.Push(.value()) - do while .match(T_COMMA) - laArray.Push(.value()) - enddo - endif - .consume(T_RBRACKET, "Expect ']' after array elements.") - - return @laArray.getArray() - endwith + local laArray + laArray = createobject("TParserInternalArray") + If !this.check(T_RBRACKET) + laArray.Push(this.value()) + do while this.match(T_COMMA) + laArray.Push(this.value()) + enddo + endif + this.consume(T_RBRACKET, "Expect ']' after array elements.") + return @laArray.getArray() endfunc Function match(tnTokenType) - With this - If .check(tnTokenType) - .advance() - Return .t. - EndIf - Return .f. - endwith + If this.check(tnTokenType) + this.advance() + Return .t. + EndIf + Return .f. EndFunc Hidden Function consume(tnTokenType, tcMessage) - With this - If .check(tnTokenType) - Return .advance() - EndIf - if empty(tcMessage) - tcMessage = "Parser Error: expected token '" + _screen.jsonUtils.tokenTypeToStr(tnTokenType) + "' got = '" + _screen.jsonUtils.tokenTypeToStr(.peek.type) + "'" - endif - error tcMessage - EndWith + If this.check(tnTokenType) + Return this.advance() + EndIf + if empty(tcMessage) + tcMessage = "Parser Error: expected token '" + _screen.jsonUtils.tokenTypeToStr(tnTokenType) + "' got = '" + _screen.jsonUtils.tokenTypeToStr(this.peek.type) + "'" + endif + error tcMessage endfunc Hidden Function check(tnTokenType) - With this - If .isAtEnd() - Return .f. - EndIf - Return .peek.type == tnTokenType - endwith + If this.isAtEnd() + Return .f. + EndIf + Return this.peek.type == tnTokenType EndFunc Hidden Function advance - With this - If !.isAtEnd() - .current = .current + 1 - EndIf - Return .tokens[.current-1] - endwith + If !this.isAtEnd() + this.current = this.current + 1 + EndIf + Return this.tokenCollection.tokens[this.current-1] endfunc Hidden Function isAtEnd - With this.peek - Return .type == T_EOF - endwith + Return this.peek.type == T_EOF endfunc Hidden Function peek_access - With this - Return .tokens[.current] - endwith + Return this.tokenCollection.tokens[this.current] endfunc Hidden Function previous_access - With this - Return .tokens[.current-1] + Return this.tokenCollection.tokens[this.current-1] + endfunc + + function CleanUp + with this + .TokenCollection = .null. + .current = 0 + .previous = .null. + .peek = .null. endwith - EndFunc + endfunc EndDefine @@ -216,16 +212,12 @@ Define Class TParserInternalArray As Custom nIndex = 0 Function Push(tvItem) - With this - .nIndex = .nIndex + 1 - Dimension .aCustomArray[.nIndex] - .aCustomArray[.nIndex] = tvItem - EndWith + this.nIndex = this.nIndex + 1 + Dimension this.aCustomArray[this.nIndex] + this.aCustomArray[this.nIndex] = tvItem Endfunc Function GetArray - With this - Return @.aCustomArray - endwith + Return @this.aCustomArray EndFunc Enddefine \ No newline at end of file diff --git a/src/testScanner.prg b/src/testScanner.prg new file mode 100644 index 0000000..a17e9d1 --- /dev/null +++ b/src/testScanner.prg @@ -0,0 +1,23 @@ +clear +cd f:\desarrollo\github\jsonfox\src\ +delete file "F:\Desarrollo\GitHub\JSONFox\trace.log" +Do loader + +lbUserJScriptTokenizer = .F. +lcClass = "JScriptScanner" +set procedure to "JScriptScanner" additive + +local lnStart +lnStart = seconds() +local loScanner +*lcFile = "F:\Desarrollo\GitHub\JSONFox\test.json" +lcFile = "c:\a1\registro-gastos\node_modules\.cache\babel-loader\38c18daa8cb0956e273a42f3cabeb7a3ca3c829d7c9614a3fabaecc14d68db02.json" +loScanner = createobject(lcClass, strconv(filetostr(lcFile),11)) +loResult = loScanner.ScanTokens() +? seconds() - lnStart +return + +for each loToken in loResult + lcOutput = loScanner.TokenStr(loToken) + loScanner.log(lcOutput) +endfor \ No newline at end of file diff --git a/src/tokenizer.prg b/src/tokenizer.prg index 17b0e8b..ec8680a 100644 --- a/src/tokenizer.prg +++ b/src/tokenizer.prg @@ -1,105 +1,93 @@ -* <> -* ========================================= -*!* Clear -*!* Cd f:\desarrollo\github\jsonfox\src\ -*!* sc = CreateObject("Tokenizer", '"string"') -*!* tokens = sc.scanTokens() -*!* For i = 1 to Alen(tokens) -*!* ? sc.tokenStr(tokens[i]) -*!* Endfor -* ========================================= -* <> - #include "JSONFox.h" * Tokenizer define class Tokenizer as custom - Hidden source - Hidden start - Hidden current - Hidden letters - Hidden hexLetters + hidden source + hidden start + hidden current + hidden letters + hidden hexLetters hidden line - - Hidden capacity - Hidden length - - Dimension tokens[1] + + hidden capacity + hidden length + + dimension tokens[1] sourceLen = 0 - - + function init(tcSource) - With this + with this .length = 1 .capacity = 0 +&& IRODG 11/08/2023 Inicio +* We remove possible invalid characters from the input source. + tcSource = strtran(tcSource, chr(0)) + tcSource = strtran(tcSource, chr(10)) + tcSource = strtran(tcSource, chr(13)) +&& IRODG 11/08/2023 Fin .source = tcSource .start = 0 .current = 1 .letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_' - .hexLetters = 'abcdefABCDEF' + .hexLetters = 'abcdefABCDEF' .line = 1 - .sourceLen = Len(tcSource) + .sourceLen = len(tcSource) endwith endfunc - Hidden function advance - With this + hidden function advance + with this .current = .current + 1 - Return substr(.source, .current-1, 1) + return substr(.source, .current-1, 1) endwith endfunc - Hidden function peek - With this - If .isAtEnd() - Return '' - EndIf + hidden function peek + with this + if .isAtEnd() + return '' + endif return substr(.source, .current, 1) endwith - EndFunc - - Hidden function peekNext - With this - If (.current + 1) > .sourceLen - Return '' - EndIf + endfunc + + hidden function peekNext + with this + if (.current + 1) > .sourceLen + return '' + endif return substr(.source, .current+1, 1) endwith - endfunc - - Hidden Function skipWhitespace - With this - LOCAL ch - Do while InList(.peek(), Chr(9), Chr(10), Chr(13), Chr(32)) + endfunc + + hidden function skipWhitespace + with this + local ch + do while inlist(.peek(), chr(9), chr(10), chr(13), chr(32)) ch = .advance() - If ch == Chr(10) + if ch == chr(10) .line = .line + 1 endif - EndDo + enddo endwith endfunc - Hidden function identifier - With this - Local lexeme - do while At(.peek(), .letters) > 0 + hidden function identifier + with this + local lexeme + do while at(.peek(), .letters) > 0 .advance() - EndDo - lexeme = Substr(.source, .start, .current-.start) -*!* If .current == Len(.source) -*!* lexeme = Substr(.source, .start, (.current+1)-.start) -*!* Else -*!* lexeme = Substr(.source, .start, .current-.start) -*!* endif + enddo + lexeme = substr(.source, .start, .current-.start) if inlist(lexeme, "true", "false", "null") return .addToken(iif(lexeme == 'null', T_NULL, T_BOOLEAN), lexeme) else .showError(.line, "Lexer Error: Unexpected identifier '" + lexeme + "'") - EndIf - EndWith + endif + endwith endfunc - Hidden function number - With this + hidden function number + with this local lexeme, isNegative lexeme = '' isNegative = (.peek() == '-') @@ -116,39 +104,50 @@ define class Tokenizer as custom do while isdigit(.peek()) .advance() enddo - EndIf - lexeme = Substr(.source, .start, .current-.start) + endif + +&& Check if number is a Scientific Notation in Tokenizer.Number() + if lower(.peek() + .peekNext()) == "e+" + .advance() + .advance() + do while isdigit(.peek()) + .advance() + enddo + endif +***************************************************************** + + lexeme = substr(.source, .start, .current-.start) return .addToken(T_NUMBER, lexeme) endwith endfunc - Hidden function string - With this - Local lexeme, ch - do while !.isAtEnd() + hidden function string + with this + local lexeme, ch + do while !.isAtEnd() ch = .peek() - Do case - case ch == '\' and InList(.peekNext(), '\', '/', 'n', 'r', 't', '"', "'") + do case + case ch == '\' and inlist(.peekNext(), '\', '/', 'n', 'r', 't', '"', "'") .advance() - Case ch = '"' + case ch = '"' .advance() - Exit - Case ch == ',' and InList(.peekNext(), '"', "'") and Type('This._anyType_') == 'C' and Alltrim(this._anyType_) == 'anyType' + exit + case ch == ',' and inlist(.peekNext(), '"', "'") and type('This._anyType_') == 'C' and alltrim(this._anyType_) == 'anyType' .advance() - Exit + exit endcase .advance() - EndDo - - lexeme = Substr(.source, .start+1, .current-.start-2) + enddo + + lexeme = substr(.source, .start+1, .current-.start-2) .escapeCharacters(@lexeme) - .checkUnicodeFormat(@lexeme) + .checkUnicodeFormat(@lexeme) return .addToken(T_STRING, lexeme) endwith - EndFunc + endfunc - Hidden function currency - With this + hidden function currency + with this local lexeme, isNegative lexeme = '' isNegative = (.peek() == '-') @@ -158,161 +157,251 @@ define class Tokenizer as custom do while isdigit(.peek()) .advance() - EndDo + enddo - * Loop while there is a comma ',' - Do while .t. - If .peek() == ',' and IsDigit(.peekNext()) +* Loop while there is a comma ',' + do while .t. + if .peek() == ',' and isdigit(.peekNext()) .advance() && eat the comma ',' do while isdigit(.peek()) .advance() - EndDo - Else + enddo + else exit - EndIf - enddo + endif + enddo - * Check for decimal part +* Check for decimal part if .peek() == '.' and isdigit(.peekNext()) .advance() && eat the dot '.' do while isdigit(.peek()) .advance() enddo - EndIf - lexeme = Substr(.source, .start+1, .current-.start) - return .addToken(T_NUMBER, Strtran(lexeme, ',')) + endif + lexeme = substr(.source, .start+1, .current-.start) + return .addToken(T_NUMBER, strtran(lexeme, ',')) endwith endfunc + procedure escapeCharacters(tcLexeme) + if len(tcLexeme) < 100 + local lcResult, i, lcChar, lcNextChar + lcResult = "" + i = 1 - Procedure escapeCharacters(tcLexeme) - * Convert all escape sequences - tcLexeme = Strtran(tcLexeme, '\\', '\') - tcLexeme = Strtran(tcLexeme, '\/', '/') - tcLexeme = Strtran(tcLexeme, '\n', Chr(10)) - tcLexeme = Strtran(tcLexeme, '\r', Chr(13)) - tcLexeme = Strtran(tcLexeme, '\t', Chr(9)) - tcLexeme = Strtran(tcLexeme, '\"', '"') - tcLexeme = Strtran(tcLexeme, "\'", "'") - EndProc - - procedure checkUnicodeFormat(tcLexeme) - * Look for unicode format - _Screen.oRegEx.Pattern = "\\u([a-fA-F0-9]{4})" - Local loResult, lcValue, i - _Screen.oRegEx.IgnoreCase = .t. - _Screen.oRegEx.global = .t. - loResult = _Screen.oRegEx.Execute(tcLexeme) - If Type('loResult') == 'O' - For i = 0 to loResult.Count-1 - lcValue = loResult.Item[i].Value - Try - tcLexeme = Strtran(tcLexeme, lcValue, Strconv(lcValue, 16)) - Catch - EndTry - EndFor - EndIf - EndProc - - Function scanTokens - With this - Dimension .tokens[1] - Do while !.isAtEnd() + do while i <= len(tcLexeme) + lcChar = substr(tcLexeme, i, 1) + if lcChar == "\" and i < len(tcLexeme) + lcNextChar = substr(tcLexeme, i + 1, 1) + do case + case lcNextChar == "\" + lcResult = lcResult + "\" + case lcNextChar == "/" + lcResult = lcResult + "/" + case lcNextChar == "n" + lcResult = lcResult + chr(10) + case lcNextChar == "r" + lcResult = lcResult + chr(13) + case lcNextChar == "t" + lcResult = lcResult + chr(9) + case lcNextChar == '"' + lcResult = lcResult + '"' + case lcNextChar == "'" + lcResult = lcResult + "'" + otherwise +* Si no es una secuencia de escape conocida, mantener ambos caracteres + lcResult = lcResult + "\" + lcNextChar + endcase + i = i + 2 && Avanzar 2 caracteres + else + lcResult = lcResult + lcChar + i = i + 1 && avanzar un carcter + endif + enddo + tcLexeme = lcResult + else + tcLexeme = strtran(tcLexeme, '\\', '\') + tcLexeme = strtran(tcLexeme, '\/', '/') + tcLexeme = strtran(tcLexeme, '\n', chr(10)) + tcLexeme = strtran(tcLexeme, '\r', chr(13)) + tcLexeme = strtran(tcLexeme, '\t', chr(9)) + tcLexeme = strtran(tcLexeme, '\"', '"') + tcLexeme = strtran(tcLexeme, "\'", "'") + endif + endproc + + procedure checkUnicodeFormat(tcLexeme) +* Look for unicode format +** This conversion is better (in performance) than Regular Expressions. +&& IRODG 09/10/2023 Inicio + local lcUnicode, lcConversion, lbReplace, lnPos + lnPos = 1 + do while .t. + lbReplace = .f. + lcUnicode = substr(tcLexeme, at('\u', tcLexeme, lnPos), 6) + if len(lcUnicode) == 6 + lbReplace = .t. + else + lcUnicode = substr(tcLexeme, at('\U', tcLexeme, lnPos), 6) + if len(lcUnicode) == 6 + lbReplace = .t. + endif + endif + if lbReplace + tcLexeme = strtran(tcLexeme, lcUnicode, strtran(strconv(lcUnicode,16), chr(0))) + else + exit + endif + enddo +&& IRODG 09/10/2023 Fin + endproc + + function scanTokens + with this + dimension .tokens[1] + do while !.isAtEnd() .skipWhitespace() .start = .current .scanToken() - EndDo + enddo .addToken(T_EOF, "") .capacity = .length-1 - - * Shrink array - Dimension .tokens[.capacity] - - Return @.tokens - EndWith + +* Shrink array + dimension .tokens[.capacity] + + local loTokens + loTokens = createobject("Empty") + addproperty(loTokens, "tokens["+alltrim(str(.capacity))+"]", null) + +* Crear una copia de los tokens + local i + for i = 1 to .capacity +* Si los tokens son objetos, crear copias profundas + if type('.tokens[i]') = 'O' + loTokens.tokens[i] = createobject("Empty") + =addproperty(loTokens.tokens[i], "type", .tokens[i].type) + =addproperty(loTokens.tokens[i], "value", .tokens[i].value) + =addproperty(loTokens.tokens[i], "line", .tokens[i].line) + else + loTokens.tokens[i] = .tokens[i] + endif + next + + .CleanUp() + + return loTokens + endwith endfunc - Hidden function scanToken - With this - Local ch - ch = .advance() - Do case + hidden function scanToken + with this + local ch + ch = .advance() + do case case ch == '{' - Return .addToken(T_LBRACE, ch) + return .addToken(T_LBRACE, ch) case ch == '}' - Return .addToken(T_RBRACE, ch) - + return .addToken(T_RBRACE, ch) + case ch == '[' - Return .addToken(T_LBRACKET, ch) + return .addToken(T_LBRACKET, ch) case ch == ']' - Return .addToken(T_RBRACKET, ch) + return .addToken(T_RBRACKET, ch) case ch == ':' - Return .addToken(T_COLON, ch) + return .addToken(T_COLON, ch) case ch == ',' - Return .addToken(T_COMMA, ch) + return .addToken(T_COMMA, ch) - Case ch == '"' - Return .string() - Case ch == '$' - Return .Currency() - Otherwise + case ch == '"' + return .string() + case ch == '$' + return .currency() + otherwise if isdigit(ch) or (ch == '-' and isdigit(.peek())) - Return .number() + return .number() endif - if At(ch, .letters) > 0 - Return .identifier() + if at(ch, .letters) > 0 + return .identifier() endif - .showError(.line, "Unknown character ['" + transform(ch) + "'], ascii: [" + TRANSFORM(ASC(ch)) + "]") - EndCase - EndWith - EndFunc + .showError(.line, "Unknown character ['" + transform(ch) + "'], ascii: [" + transform(asc(ch)) + "]") + endcase + endwith + endfunc hidden function addToken(tnTokenType, tcTokenValue) - With this + with this .checkCapacity() + local loToken loToken = createobject("Empty") =addproperty(loToken, "type", tnTokenType) =addproperty(loToken, "value", tcTokenValue) - =AddProperty(loToken, "line", .line) - + =addproperty(loToken, "line", .line) + .tokens[.length] = loToken .length = .length + 1 - EndWith - EndFunc - - Hidden function checkCapacity - With this - If .capacity < .length + 1 - If Empty(.capacity) + endwith + endfunc + + hidden function checkCapacity + with this + if .capacity < .length + 1 + if empty(.capacity) .capacity = 8 - Else + else .capacity = .capacity * 2 - EndIf - Dimension .tokens[.capacity] - EndIf + endif + dimension .tokens[.capacity] + endif endwith endfunc function showError(tnLine, tcMessage) - error "SYNTAX ERROR: (" + TRANSFORM(tnLine) + ":" + TRANSFORM(this.current) + ")" + tcMessage + error "SYNTAX ERROR: (" + transform(tnLine) + ":" + transform(this.current) + ")" + tcMessage endfunc function isAtEnd - With this - return .current > .sourceLen - EndWith + with this + return .current > .sourceLen + endwith endfunc function tokenStr(toToken) local lcType, lcValue lcType = _screen.jsonUtils.tokenTypeToStr(toToken.type) - lcValue = alltrim(transform(toToken.value)) - return "Token(" + lcType + ", '" + lcValue + "') at Line(" + Alltrim(Str(toToken.Line)) + ")" - EndFunc -enddefine \ No newline at end of file + lcValue = alltrim(transform(toToken.value)) + return "Token(" + lcType + ", '" + lcValue + "') at Line(" + alltrim(str(toToken.line)) + ")" + endfunc + + function CleanUp + with this +* Liberar el array de tokens + if type('this.tokens', 1) == 'A' and alen(this.tokens) > 1 + local i + for i = 1 to alen(this.tokens) + if type('this.tokens[i]') = 'O' +* Liberar propiedades del objeto token + this.tokens[i] = .null. + endif + next +* Redimensionar el array a tamao mnimo + dimension this.tokens[1] + this.tokens[1] = .null. + endif + +* Liberar otras variables que puedan ocupar mucha memoria + this.source = "" + this.sourceLen = 0 + this.capacity = 0 + this.length = 1 + endwith + return .t. + endfunc + +enddefine