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..e2cdb54 100644 Binary files a/JSONFox.PJT and b/JSONFox.PJT differ diff --git a/JSONFox.pjx b/JSONFox.pjx index d3af685..728ac59 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..c845ccc 100644 Binary files a/jsonfox.app and b/jsonfox.app differ diff --git a/src/arraytocursor.prg b/src/arraytocursor.prg index 1b89124..f4721bd 100644 --- a/src/arraytocursor.prg +++ b/src/arraytocursor.prg @@ -114,7 +114,7 @@ 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 @@ -125,9 +125,16 @@ Define Class ArrayToCursor As Session lxValue = .Value() lcType = Vartype(lxValue) lnFieldLength = 0 + lnDecimals = 0 Do Case Case lcType == 'N' - lcType = Iif(Occurs('.', Transform(lxValue)) > 0, 'N', 'I') + lcType = Iif(Occurs('.', Transform(lxValue)) > 0 or lxValue > INTEGER_MAX_CAPACITY, 'N', 'I') + && >>>>>>> IRODG 03/17/24 + If lcType == 'N' + lnFieldLength = Len(Transform(lxValue)) + lnDecimals = len(GetWordNum(Transform(lxValue),2,'.')) + EndIf + && <<<<<<< IRODG 03/17/24 Case lcType == 'C' If Len(lxValue) > STRING_MAX_SIZE lcType = 'M' @@ -140,7 +147,7 @@ Define Class ArrayToCursor As Session 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") @@ -218,8 +225,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 +242,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 +263,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 diff --git a/src/cursortoarray.prg b/src/cursortoarray.prg index 3b414ea..327db3f 100644 --- a/src/cursortoarray.prg +++ b/src/cursortoarray.prg @@ -2,6 +2,12 @@ define class CursorToArray as session nSessionID = 0 CurName = "" + && IRODG 07/10/2023 Inicio + ParseUTF8 = .f. + && IRODG 07/10/2023 Fin + && IRODG 27/10/2023 Inicio + TrimChars = .F. + && IRODG 27/10/2023 Fin * Function CursorToArray function CursorToArray as memo if !empty(this.nSessionID) @@ -9,7 +15,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 +53,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,10 +70,25 @@ define class CursorToArray as session else lcValue = 'null' endif - otherwise - lcValue = JSONUtils.GetString(alltrim(lcValue)) + Otherwise + && IRODG 08/08/2023 Inicio + *lcValue = JSONUtils.GetString(alltrim(lcValue)) + && IRODG 07/10/2023 Inicio +*!* lcValue = JSONUtils.GetString(lcValue) + && IRODG 27/10/2023 Inicio + * lcValue = JSONUtils.GetString(lcValue, this.ParseUTF8) + lcValue = JSONUtils.GetString(Iif(this.TrimChars, Alltrim(lcValue), lcValue), this.ParseUTF8) + && IRODG 27/10/2023 Fin + && IRODG 07/10/2023 Fin + && IRODG 08/08/2023 Fin endcase - lcOutput = lcOutput + alltrim(lcValue) + && IRODG 08/08/2023 Inicio + *lcOutput = lcOutput + alltrim(lcValue) + && IRODG 27/10/2023 Inicio +*!* lcOutput = lcOutput + lcValue + lcOutput = lcOutput + Iif(this.TrimChars, Alltrim(lcValue), lcValue) + && IRODG 27/10/2023 Fin + && IRODG 08/08/2023 Fin case aColumns[i, 2] $ "YFIN" lcOutput = lcOutput + transform(lcValue) case aColumns[i, 2] = "L" 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.scx b/src/frmjsonviewer.scx index 0b3951e..0bdc70b 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..cbc08ff 100644 --- a/src/jsonclass.prg +++ b/src/jsonclass.prg @@ -4,7 +4,7 @@ define class JSONClass as session LastErrorText = "" lError = .f. lShowErrors = .t. - version = "9.7" + version = "10.6" hidden lInternal hidden lTablePrompt Dimension aCustomArray[1] @@ -12,6 +12,10 @@ define class JSONClass as session * Set this property to .T. if you want the lexer uses JSONFoxHelper.dll NETScanner = .f. && <<<<<<< IRODG 07/01/21 + + && >>>>>>> IRODG 02/27/24 + JScriptScanner = .f. + && <<<<<<< IRODG 02/27/24 *Function Init function init @@ -32,11 +36,14 @@ define class JSONClass as session try 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() @@ -56,11 +63,40 @@ define class JSONClass as session Else return loJSONObj EndIf - endfunc + ENDFUNC + + * tokenize + FUNCTION dumpTokens + lparameters tcJsonStr as memo, tcOutput as string + try + this.ResetError() + local lexer + do case + case this.NETScanner + lexer = createobject("NetScanner", tcJsonStr) + case this.JScriptScanner + lexer = createobject("JScriptScanner", tcJsonStr) + otherwise + lexer = createobject("Tokenizer", tcJsonStr) + endcase + Local laTokens + laTokens = lexer.scanTokens() + IF FILE(tcOutput) + DELETE FILE (tcOutput) + ENDIF + FOR EACH loToken IN laTokens + STRTOFILE(tokenStr(loToken), tcOutput, 1) + ENDFOR + catch to loEx + this.ShowExceptionError(loEx) + finally + release lexer, laTokens + 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 lcTypeFlag = type('tcFlags') @@ -79,12 +115,11 @@ define class JSONClass as session release objToJson endtry endif - try local lexer, parser lexer = createobject("Tokenizer", tvNewVal) parser = createobject("JSONStringify", lexer) - loJSONStr = parser.Stringify(llParseUtf8) + loJSONStr = parser.Stringify(llParseUtf8, tlTrimChars) catch to loEx this.ShowExceptionError(loEx) finally @@ -148,10 +183,10 @@ define class JSONClass as session && Function Encode && <> please use Stringify function instead. && ======================================================================== && - function Encode(toObj as object, tcFlags as string) as memo + function Encode(toObj as object, tcFlags as string, tlUtf8 as Boolean, tlTrimChars as Boolean) as memo local loEncode loEncode = createobject("ObjectToJson") - return loEncode.Encode(@toObj, tcFlags) + return loEncode.Encode(@toObj, tcFlags, tlUtf8, tlTrimChars) endfunc && ======================================================================== && && Function decode @@ -203,9 +238,11 @@ define class JSONClass as session try this.ResetError() =xmltocursor(tcXML, 'qXML') - loParser = createobject("CursorToArray") - loParser.CurName = "qXML" + loParser = createobject("CursorToArray") + loParser.CurName = "qXML" loParser.nSessionID = set("Datasession") + loParser.ParseUTF8 = .T. + loParser.TrimChars = .T. lcJsonXML = loParser.CursorToArray() catch to loEx this.ShowExceptionError(loEx) @@ -218,7 +255,7 @@ define class JSONClass as session endfunc * CursorToJSON function CursorToJSON as memo - lparameters tcCursor as string, tbCurrentRow as Boolean, tnDataSession as integer, tlJustArray as Boolean + 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 lcJsonXML = '' lcCursor = SYS(2015) @@ -236,6 +273,12 @@ 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) @@ -246,10 +289,69 @@ define class JSONClass as session endtry lcOutput = iif(tlJustArray, lcJsonXML, '{"' + lower(alltrim(tcCursor)) + '":' + lcJsonXML + '}') return lcOutput - endfunc + EndFunc + + * CursorToJSONObject + function CursorToJSONObject(tcCursor as string, tbCurrentRow as Boolean, tnDataSession as integer) as object + local loParser, lcCursor, lnRecno, loResult as Variant + lcCursor = SYS(2015) + try + 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 + 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 + Try + 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 + 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 + Try local lexer, parser this.ResetError() if !empty(tcCursor) @@ -292,7 +394,7 @@ define class JSONClass as session return lcOutput endfunc * tokenize - function dumpTokens + function dumpTokens2 lparameters tcJsonStr as memo try this.ResetError() 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..15cda23 100644 --- a/src/jsonstringify.prg +++ b/src/jsonstringify.prg @@ -10,6 +10,7 @@ define class JSONStringify as custom ParseUtf8 = .f. + TrimChars = .f. Dimension tokens[1] Hidden current @@ -25,8 +26,10 @@ define class JSONStringify as custom * Stringify function Stringify as memo - lparameters tlParseUtf8 + lparameters tlParseUtf8, tlTrimChars this.ParseUtf8 = tlParseUtf8 + this.TrimChars = tlTrimChars + return this.value(0) endfunc && ======================================================================== && @@ -62,9 +65,11 @@ 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 = this.previous.value + 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 +78,8 @@ 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(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 diff --git a/src/jsonutils.prg b/src/jsonutils.prg index 5b81cc1..4c0396b 100644 --- a/src/jsonutils.prg +++ b/src/jsonutils.prg @@ -5,24 +5,45 @@ && ======================================================================== && define class jsonutils as custom - Dimension aPattern[5, 2] + 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 @@ -30,8 +51,8 @@ define class jsonutils as custom && ======================================================================== && && Function GetValue && ======================================================================== && - function getvalue as string - lparameters tcvalue as string, tctype as character + function getValue as string + lparameters tcvalue as string, tctype as character, tlParseUTF8 as Boolean, tlTrimChars as Boolean do case case tctype $ "CDTBGMQVWX" do case @@ -42,9 +63,12 @@ define class jsonutils as custom Case tctype == 'X' tcvalue = "null" Otherwise - tcvalue = this.getstring(tcvalue) +*!* tcvalue = this.getstring(tcvalue) + tcValue = this.getString(Iif(tlTrimChars, Alltrim(tcValue), tcValue), tlParseUTF8) endcase - tcvalue = alltrim(tcvalue) + && IRODG 08/08/2023 Inicio + *tcvalue = alltrim(tcValue) + && IRODG 08/08/2023 Fin case tctype $ "YFIN" tcvalue = strtran(transform(tcvalue), ',', '.') case tctype == 'L' @@ -66,7 +90,7 @@ define class jsonutils as custom 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 @@ -99,10 +123,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 = {//::} @@ -132,11 +163,16 @@ define class jsonutils as custom && ======================================================================== && function getstring as string lparameters tcString as string, tlParseUtf8 as Boolean - tcString = allt(tcString) + && 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 @@ -173,9 +209,12 @@ define class jsonutils as custom tcString = StrTran(tcString,'','\u00a9') tcString = StrTran(tcString,'','\u00ae') tcString = StrTran(tcString,'','\u00e7') - endif - - return '"' +tcString + '"' + tcString = StrTran(tcString,'','\u00ba') + EndIf + If Left(Alltrim(tcString), 1) != '"' and Right(Alltrim(tcString),1) != '"' + return '"'+tcString+'"' + EndIf + Return tcString endfunc && ======================================================================== && && Function CheckProp 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..4d6a032 100644 --- a/src/objecttojson.prg +++ b/src/objecttojson.prg @@ -6,6 +6,8 @@ define class ObjectToJSON as session cDateAct = '' nOrden = 0 cFlags = '' + parseUTF8 = .f. + TrimChars = .f. * Function Init function init this.lCentury = set("Century") == "OFF" @@ -15,7 +17,7 @@ define class ObjectToJSON as session mvcount = 60000 endfunc * Encode - function Encode(toRefObj, tcFlags) + function Encode(toRefObj, tcFlags, tlParseUTF8, tlTrimChars) lPassByRef = .t. try external array toRefObj @@ -23,12 +25,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 @@ -46,8 +55,9 @@ define class ObjectToJSON as session for k = 1 to alen(tValue) lcArray = lcArray + iif(len(lcArray) > 1, ',', '') try + local array laLista[1] *local array aLista(alen(tValue[k])) - =acopy(tValue[k], aLista) + =acopy(tValue[k], laLista) lcArray = lcArray + this.AnyToJson(@aLista) catch lcArray = lcArray + this.AnyToJson(tValue[k]) @@ -67,7 +77,8 @@ define class ObjectToJSON as session lcArray = lcArray + ',' endif try - =acopy(tValue[k, j], aLista) + local array laLista[1] + =acopy(tValue[k, j], laLista) lcArray = lcArray + this.AnyToJson(@aLista) catch lcArray = lcArray + this.AnyToJson(tValue[k, j]) @@ -91,8 +102,9 @@ define class ObjectToJSON as session 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) + local array laLista[1] + =acopy(tValue. &gaMembers[j], laLista) + lcJSONStr = lcJSONStr + this.AnyToJson(@laLista) catch try lcJSONStr = lcJSONStr + this.AnyToJson(tValue. &gaMembers[j]) @@ -121,7 +133,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..0820f19 100644 --- a/src/parser.prg +++ b/src/parser.prg @@ -14,18 +14,14 @@ define class Parser as custom Hidden peek function init(toScanner) - With this - Local laTokens - laTokens = toScanner.scanTokens() - =Acopy(laTokens, .tokens) - .current = 1 - endwith + Local laTokens + laTokens = toScanner.scanTokens() + =Acopy(laTokens, this.tokens) + this.current = 1 endfunc function Parse - With this - Return .value() - endwith + Return this.value() endfunc && ======================================================================== && && Function Object @@ -33,51 +29,41 @@ 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) @@ -97,113 +83,94 @@ 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.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.tokens[this.current] endfunc Hidden Function previous_access - With this - Return .tokens[.current-1] - endwith + Return this.tokens[this.current-1] EndFunc EndDefine @@ -216,16 +183,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..7fe12ad 100644 --- a/src/tokenizer.prg +++ b/src/tokenizer.prg @@ -1,15 +1,3 @@ -* <> -* ========================================= -*!* 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 @@ -26,11 +14,16 @@ define class Tokenizer as custom Dimension tokens[1] sourceLen = 0 - 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 .start = 0 .current = 1 @@ -116,7 +109,18 @@ define class Tokenizer as custom do while isdigit(.peek()) .advance() enddo - EndIf + 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 @@ -185,33 +189,97 @@ define class Tokenizer as custom endfunc - 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 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 escapeCharacters(tcLexeme) + local lcResult, i, lcChar, lcNextChar + lcResult = "" + i = 1 - procedure checkUnicodeFormat(tcLexeme) + 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 + 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 + ** 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 +*!* _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 +*!* && IRODG 09/10/2023 Inicio +*!* ** Replace null character (chr(0)) from conversion result (strconv(lcValue, 16)) +*!* *!* tcLexeme = Strtran(tcLexeme, lcValue, (strconv(lcValue, 16))) +*!* tcLexeme = Strtran(tcLexeme, lcValue, strtran((strconv(lcValue, 16)), chr(0))) +*!* && IRODG 09/10/2023 Fin +*!* Catch +*!* EndTry +*!* EndFor +*!* EndIf EndProc Function scanTokens