From cbfc7241e8098fbfce1fd96c0862c2b27728bfd6 Mon Sep 17 00:00:00 2001 From: little-brother Date: Sat, 16 Oct 2021 12:41:59 +0300 Subject: [PATCH] initial --- LICENSE | 339 ++++++++++++ README.md | 15 + main.c | 1515 ++++++++++++++++++++++++++++++++++++++++++++++++++++ xml.c | 818 ++++++++++++++++++++++++++++ xml.h | 59 ++ xmltab.png | Bin 0 -> 29878 bytes 6 files changed, 2746 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 main.c create mode 100644 xml.c create mode 100644 xml.h create mode 100644 xmltab.png diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6efb5c2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..cf280d2 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +**xmltab-wlx** is a [Total Commander](https://www.ghisler.com/) plugin to view XML files. + +|[**Download the latest version**](https://github.com/little-brother/xmltab-wlx/releases/latest/download/xmltab.zip)| +|-------------------------------------------------------------------------------------------| + +![View](xmltab.png) + +### Features +* Mixed tree and grid view +* Column filters +* Sort data by column click +* Beautifier and highlighting +* Supports ANSI, UTF8 and UTF16 + +If you have any problems, comments or suggestions, check [Wiki](https://github.com/little-brother/xmltab-wlx/wiki), create [issue](https://github.com/little-brother/xmltab-wlx/issues) or just let me know lb.im@ya.ru. \ No newline at end of file diff --git a/main.c b/main.c new file mode 100644 index 0000000..ba58e7a --- /dev/null +++ b/main.c @@ -0,0 +1,1515 @@ +#define UNICODE +#define _UNICODE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "xml.h" + +#define LVS_EX_AUTOSIZECOLUMNS 0x10000000 + +#define WMU_UPDATE_GRID WM_USER + 1 +#define WMU_UPDATE_RESULTSET WM_USER + 2 +#define WMU_UPDATE_FILTER_SIZE WM_USER + 3 +#define WMU_AUTO_COLUMN_SIZE WM_USER + 4 +#define WMU_RESET_CACHE WM_USER + 5 +#define WMU_SET_FONT WM_USER + 6 +#define WMU_UPDATE_TEXT WM_USER + 7 +#define WMU_UPDATE_HIGHLIGHT WM_USER + 8 + +#define IDC_MAIN 100 +#define IDC_TREE 101 +#define IDC_TAB 102 +#define IDC_GRID 103 +#define IDC_TEXT 104 +#define IDC_STATUSBAR 105 +#define IDC_HEADER_EDIT 1000 + +#define IDM_COPY_CELL 5000 +#define IDM_COPY_ROW 5001 +#define IDM_COPY_TEXT 5002 +#define IDM_SELECTALL 5003 +#define IDM_FORMAT 5004 + +#define IDH_EXIT 6000 +#define IDH_NEXT 6001 +#define IDH_PREV 6002 + +#define SB_UNUSED 0 +#define SB_CODEPAGE 1 +#define SB_MODE 2 +#define SB_ROW_COUNT 3 +#define SB_CURRENT_ROW 4 + +#define MAX_LENGTH 4096 +#define MAX_HIGHLIGHT_LENGTH 64000 +#define APP_NAME TEXT("xmltab") +#define LOADING TEXT("Loading...") + +#define CP_UTF16LE 1200 +#define CP_UTF16BE 1201 + +typedef struct { + int size; + DWORD PluginInterfaceVersionLow; + DWORD PluginInterfaceVersionHi; + char DefaultIniName[MAX_PATH]; +} ListDefaultParamStruct; + +static TCHAR iniPath[MAX_PATH] = {0}; + +LRESULT CALLBACK cbNewMain (HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); +LRESULT CALLBACK cbNewFilterEdit(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); +LRESULT CALLBACK cbNewText(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); +HTREEITEM addNode(HWND hTreeWnd, HTREEITEM hParentItem, xml_element* val); +void highlightText(HWND hWnd, TCHAR* text); +char* formatXML(const char* data); +void setStoredValue(TCHAR* name, int value); +int getStoredValue(TCHAR* name, int defValue); +int CALLBACK cbEnumTabStopChildren (HWND hWnd, LPARAM lParam); +TCHAR* utf8to16(const char* in); +char* utf16to8(const TCHAR* in); +int detectCodePage(const unsigned char *data); +void setClipboardText(const TCHAR* text); +BOOL isNumber(const TCHAR* val); +BOOL isEmpty (const char* s); +BOOL isUtf8(const char * string); +HTREEITEM TreeView_AddItem (HWND hTreeWnd, TCHAR* caption, HTREEITEM parent, LPARAM lParam); +int TreeView_GetItemText(HWND hTreeWnd, HTREEITEM hItem, TCHAR* buf, int maxLen); +LPARAM TreeView_GetItemParam(HWND hTreeWnd, HTREEITEM hItem); +int TreeView_SetItemText(HWND hTreeWnd, HTREEITEM hItem, TCHAR* text); +int ListView_AddColumn(HWND hListWnd, TCHAR* colName); +int Header_GetItemText(HWND hWnd, int i, TCHAR* pszText, int cchTextMax); + +BOOL APIENTRY DllMain (HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { + return TRUE; +} + +void __stdcall ListGetDetectString(char* DetectString, int maxlen) { + snprintf(DetectString, maxlen, "MULTIMEDIA & ext=\"XML\""); +} + +void __stdcall ListSetDefaultParams(ListDefaultParamStruct* dps) { + if (iniPath[0] == 0) { + DWORD size = MultiByteToWideChar(CP_ACP, 0, dps->DefaultIniName, -1, NULL, 0); + MultiByteToWideChar(CP_ACP, 0, dps->DefaultIniName, -1, iniPath, size); + } +} + +HWND APIENTRY ListLoad (HWND hListerWnd, char* fileToLoad, int showFlags) { + DWORD size = MultiByteToWideChar(CP_ACP, 0, fileToLoad, -1, NULL, 0); + TCHAR* filepath = (TCHAR*)calloc (size, sizeof (TCHAR)); + MultiByteToWideChar(CP_ACP, 0, fileToLoad, -1, filepath, size); + + struct _stat st = {0}; + if (_tstat(filepath, &st) != 0 || st.st_size > getStoredValue(TEXT("max-file-size"), 100000000) || st.st_size < 4) + return 0; + + char* data = calloc(st.st_size + 1, sizeof(char)); + FILE *f = fopen(fileToLoad, "rb"); + fread(data, sizeof(char), st.st_size, f); + fclose(f); + + int cp = detectCodePage(data); + if (cp == CP_UTF16BE) { + for (int i = 0; i < st.st_size/2; i++) { + int c = data[2 * i]; + data[2 * i] = data[2 * i + 1]; + data[2 * i + 1] = c; + } + } + + if (cp == CP_UTF16LE || cp == CP_UTF16BE) { + char* data8 = utf16to8((TCHAR*)data); + free(data); + data = data8; + } + + if (cp == CP_ACP) { + DWORD len = MultiByteToWideChar(CP_ACP, 0, data, -1, NULL, 0); + TCHAR* data16 = (TCHAR*)calloc (len, sizeof (TCHAR)); + MultiByteToWideChar(CP_ACP, 0, data, -1, data16, len); + free(data); + char* data8 = utf16to8((TCHAR*)data16); + data = data8; + free(data16); + } + + struct xml_element *xml = xml_parse(data); + if (!xml) { + free(data); + return 0; + } + + INITCOMMONCONTROLSEX icex; + icex.dwSize = sizeof(icex); + icex.dwICC = ICC_LISTVIEW_CLASSES | ICC_TREEVIEW_CLASSES; + InitCommonControlsEx(&icex); + LoadLibrary(TEXT("msftedit.dll")); + + BOOL isStandalone = GetParent(hListerWnd) == HWND_DESKTOP; + HWND hMainWnd = CreateWindowEx(WS_EX_CONTROLPARENT, WC_STATIC, TEXT("xml-wlx"), WS_CHILD | WS_VISIBLE | (isStandalone ? SS_SUNKEN : 0), + 0, 0, 100, 100, hListerWnd, (HMENU)IDC_MAIN, GetModuleHandle(0), NULL); + + SetProp(hMainWnd, TEXT("WNDPROC"), (HANDLE)SetWindowLongPtr(hMainWnd, GWLP_WNDPROC, (LONG_PTR)&cbNewMain)); + SetProp(hMainWnd, TEXT("XML"), xml); + SetProp(hMainWnd, TEXT("DATA"), data); + SetProp(hMainWnd, TEXT("CACHE"), 0); + SetProp(hMainWnd, TEXT("RESULTSET"), 0); + SetProp(hMainWnd, TEXT("ROWCOUNT"), calloc(1, sizeof(int))); + SetProp(hMainWnd, TEXT("TOTALROWCOUNT"), calloc(1, sizeof(int))); + SetProp(hMainWnd, TEXT("ORDERBY"), calloc(1, sizeof(int))); + SetProp(hMainWnd, TEXT("COLNO"), calloc(1, sizeof(int))); + SetProp(hMainWnd, TEXT("SPLITTERWIDTH"), calloc(1, sizeof(int))); + SetProp(hMainWnd, TEXT("ISFORMAT"), calloc(1, sizeof(int))); + SetProp(hMainWnd, TEXT("FONT"), 0); + SetProp(hMainWnd, TEXT("FONTSIZE"), calloc(1, sizeof(int))); + SetProp(hMainWnd, TEXT("GRAYBRUSH"), CreateSolidBrush(GetSysColor(COLOR_BTNFACE))); + SetProp(hMainWnd, TEXT("MAXHIGHLIGHTLENGTH"), calloc(1, sizeof(int))); + + *(int*)GetProp(hMainWnd, TEXT("SPLITTERWIDTH")) = getStoredValue(TEXT("splitter-width"), 200); + *(int*)GetProp(hMainWnd, TEXT("FONTSIZE")) = getStoredValue(TEXT("font-size"), 16); + *(int*)GetProp(hMainWnd, TEXT("ISFORMAT")) = getStoredValue(TEXT("format"), 1); + *(int*)GetProp(hMainWnd, TEXT("MAXHIGHLIGHTLENGTH")) = getStoredValue(TEXT("max-highlight-length"), 64000); + + HWND hStatusWnd = CreateStatusWindow(WS_CHILD | WS_VISIBLE | (isStandalone ? SBARS_SIZEGRIP : 0), NULL, hMainWnd, IDC_STATUSBAR); + int sizes[6] = {90, 150, 200, 400, 500, -1}; + SendMessage(hStatusWnd, SB_SETPARTS, 6, (LPARAM)&sizes); + + HWND hTreeWnd = CreateWindow(WC_TREEVIEW, NULL, WS_VISIBLE | WS_CHILD | TVS_HASBUTTONS | TVS_HASLINES | TVS_SHOWSELALWAYS | WS_TABSTOP, + 0, 0, 100, 100, hMainWnd, (HMENU)IDC_TREE, GetModuleHandle(0), NULL); + + HWND hTabWnd = CreateWindow(WC_TABCONTROL, NULL, WS_CHILD | WS_CLIPSIBLINGS | WS_VISIBLE | WS_TABSTOP, 100, 100, 100, 100, + hMainWnd, (HMENU)IDC_TAB, GetModuleHandle(0), NULL); + + TCITEM tci; + tci.mask = TCIF_TEXT | TCIF_IMAGE; + tci.iImage = -1; + tci.pszText = TEXT("Grid"); + tci.cchTextMax = 5; + TabCtrl_InsertItem(hTabWnd, 0, &tci); + + tci.pszText = TEXT("Text"); + tci.cchTextMax = 5; + TabCtrl_InsertItem(hTabWnd, 1, &tci); + + int tabNo = getStoredValue(TEXT("tab-no"), 0); + HWND hGridWnd = CreateWindow(WC_LISTVIEW, NULL, (tabNo == 0 ? WS_VISIBLE : 0) | WS_CHILD | LVS_REPORT | LVS_SHOWSELALWAYS | LVS_SINGLESEL | LVS_OWNERDATA | WS_TABSTOP, + 205, 0, 100, 100, hTabWnd, (HMENU)IDC_GRID, GetModuleHandle(0), NULL); + ListView_SetExtendedListViewStyle(hGridWnd, LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES | LVS_EX_LABELTIP); + + HWND hHeader = ListView_GetHeader(hGridWnd); + LONG_PTR styles = GetWindowLongPtr(hHeader, GWL_STYLE); + SetWindowLongPtr(hHeader, GWL_STYLE, styles | HDS_FILTERBAR); + + HWND hTextWnd = CreateWindowEx(0, TEXT("RICHEDIT50W"), NULL, (tabNo == 1 ? WS_VISIBLE : 0) | WS_CHILD | ES_MULTILINE | ES_AUTOVSCROLL | ES_AUTOHSCROLL | ES_WANTRETURN | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP | ES_NOHIDESEL | ES_READONLY, + 0, 0, 100, 100, hTabWnd, (HMENU)IDC_TEXT, GetModuleHandle(0), NULL); + SetProp(hTextWnd, TEXT("WNDPROC"), (HANDLE)SetWindowLongPtr(hTextWnd, GWLP_WNDPROC, (LONG_PTR)cbNewText)); + TabCtrl_SetCurSel(hTabWnd, tabNo); + + HMENU hDataMenu = CreatePopupMenu(); + AppendMenu(hDataMenu, MF_STRING, IDM_COPY_CELL, TEXT("Copy cell")); + AppendMenu(hDataMenu, MF_STRING, IDM_COPY_ROW, TEXT("Copy row")); + SetProp(hMainWnd, TEXT("DATAMENU"), hDataMenu); + + HMENU hTextMenu = CreatePopupMenu(); + AppendMenu(hTextMenu, MF_STRING, IDM_COPY_TEXT, TEXT("Copy")); + AppendMenu(hTextMenu, MF_STRING, IDM_SELECTALL, TEXT("Select all")); + AppendMenu(hTextMenu, MF_STRING, 0, NULL); + AppendMenu(hTextMenu, MF_STRING | (*(int*)GetProp(hMainWnd, TEXT("ISFORMAT")) != 0 ? MF_CHECKED : 0), IDM_FORMAT, TEXT("Format")); + SetProp(hMainWnd, TEXT("TEXTMENU"), hTextMenu); + + SendMessage(hMainWnd, WMU_SET_FONT, 0, 0); + SendMessage(hMainWnd, WM_SIZE, 0, 0); + + SendMessage(hStatusWnd, SB_SETTEXT, SB_CODEPAGE, + (LPARAM)(cp == CP_UTF8 ? TEXT(" UTF-8") : cp == CP_UTF16LE ? TEXT(" UTF-16LE") : cp == CP_UTF16BE ? TEXT(" UTF-16BE") : TEXT(" ANSI"))); + + xml_element* node = xml->first_child; + while (node) { + HTREEITEM hItem = addNode(hTreeWnd, TVI_ROOT, node); + TreeView_Expand(hTreeWnd, hItem, TVE_EXPAND); + node = node->next; + } + + HTREEITEM hItem = TreeView_GetNextItem(hTreeWnd, TVI_ROOT, TVGN_CHILD); + TreeView_Select(hTreeWnd, hItem, TVGN_CARET); + + RegisterHotKey(hMainWnd, IDH_EXIT, 0, VK_ESCAPE); + RegisterHotKey(hMainWnd, IDH_NEXT, 0, VK_TAB); + RegisterHotKey(hMainWnd, IDH_PREV, MOD_CONTROL, VK_TAB); + SetFocus(hTreeWnd); + + return hMainWnd; +} + +void __stdcall ListCloseWindow(HWND hWnd) { + setStoredValue(TEXT("splitter-width"), *(int*)GetProp(hWnd, TEXT("SPLITTERWIDTH"))); + setStoredValue(TEXT("font-size"), *(int*)GetProp(hWnd, TEXT("FONTSIZE"))); + setStoredValue(TEXT("format"), *(int*)GetProp(hWnd, TEXT("ISFORMAT"))); + setStoredValue(TEXT("tab-no"), TabCtrl_GetCurSel(GetDlgItem(hWnd, IDC_TAB))); + + SendMessage(hWnd, WMU_RESET_CACHE, 0, 0); + xml_free((xml_element*)GetProp(hWnd, TEXT("XML"))); + free((char*)GetProp(hWnd, TEXT("DATA"))); + free((int*)GetProp(hWnd, TEXT("ROWCOUNT"))); + free((int*)GetProp(hWnd, TEXT("TOTALROWCOUNT"))); + free((int*)GetProp(hWnd, TEXT("ORDERBY"))); + free((int*)GetProp(hWnd, TEXT("COLNO"))); + free((int*)GetProp(hWnd, TEXT("SPLITTERWIDTH"))); + free((int*)GetProp(hWnd, TEXT("ISFORMAT"))); + free((int*)GetProp(hWnd, TEXT("MAXHIGHLIGHTLENGTH"))); + + DeleteFont(GetProp(hWnd, TEXT("FONT"))); + DeleteObject(GetProp(hWnd, TEXT("GRAYBRUSH"))); + DestroyMenu(GetProp(hWnd, TEXT("DATAMENU"))); + DestroyMenu(GetProp(hWnd, TEXT("TEXTMENU"))); + + RemoveProp(hWnd, TEXT("WNDPROC")); + RemoveProp(hWnd, TEXT("CACHE")); + RemoveProp(hWnd, TEXT("RESULTSET")); + RemoveProp(hWnd, TEXT("ROWCOUNT")); + RemoveProp(hWnd, TEXT("TOTALROWCOUNT")); + RemoveProp(hWnd, TEXT("ORDERBY")); + RemoveProp(hWnd, TEXT("COLNO")); + RemoveProp(hWnd, TEXT("XML")); + RemoveProp(hWnd, TEXT("DATA")); + RemoveProp(hWnd, TEXT("SPLITTERWIDTH")); + RemoveProp(hWnd, TEXT("ISFORMAT")); + RemoveProp(hWnd, TEXT("MAXHIGHLIGHTLENGTH")); + + RemoveProp(hWnd, TEXT("FONT")); + RemoveProp(hWnd, TEXT("GRAYBRUSH")); + RemoveProp(hWnd, TEXT("DATAMENU")); + RemoveProp(hWnd, TEXT("TEXTMENU")); + + DestroyWindow(hWnd); + return; +} + +LRESULT CALLBACK cbNewMain(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { + switch (msg) { + case WM_HOTKEY: { + WPARAM id = wParam; + if (id == IDH_EXIT) + SendMessage(GetParent(hWnd), WM_CLOSE, 0, 0); + + if (id == IDH_NEXT || id == IDH_PREV) { + HWND hFocus = GetFocus(); + HWND wnds[1000] = {0}; + EnumChildWindows(hWnd, (WNDENUMPROC)cbEnumTabStopChildren, (LPARAM)wnds); + + int no = 0; + while(wnds[no] && wnds[no] != hFocus) + no++; + + int cnt = no; + while(wnds[cnt]) + cnt++; + + BOOL isBackward = id == IDH_PREV; + no += isBackward ? -1 : 1; + SetFocus(wnds[no] && no >= 0 ? wnds[no] : (isBackward ? wnds[cnt - 1] : wnds[0])); + } + } + break; + + case WM_SIZE: { + HWND hStatusWnd = GetDlgItem(hWnd, IDC_STATUSBAR); + SendMessage(hStatusWnd, WM_SIZE, 0, 0); + RECT rc; + GetClientRect(hStatusWnd, &rc); + int statusH = rc.bottom; + + int splitterW = *(int*)GetProp(hWnd, TEXT("SPLITTERWIDTH")); + GetClientRect(hWnd, &rc); + HWND hTreeWnd = GetDlgItem(hWnd, IDC_TREE); + HWND hTabWnd = GetDlgItem(hWnd, IDC_TAB); + HWND hGridWnd = GetDlgItem(hTabWnd, IDC_GRID); + HWND hTextWnd = GetDlgItem(hTabWnd, IDC_TEXT); + SetWindowPos(hTreeWnd, 0, 0, 0, splitterW, rc.bottom - statusH, SWP_NOMOVE | SWP_NOZORDER); + SetWindowPos(hTabWnd, 0, splitterW + 5, 0, rc.right - splitterW - 5, rc.bottom - statusH, SWP_NOZORDER); + + RECT rc2; + GetClientRect(hTabWnd, &rc); + TabCtrl_GetItemRect(hTabWnd, 0, &rc2); + SetWindowPos(hTextWnd, 0, 2, rc2.bottom + 3, rc.right - rc.left - 5, rc.bottom - rc2.bottom - 7, SWP_NOZORDER); + SetWindowPos(hGridWnd, 0, 2, rc2.bottom + 3, rc.right - rc.left - 5, rc.bottom - rc2.bottom - 7, SWP_NOZORDER); + } + break; + + case WM_PAINT: { + PAINTSTRUCT ps = {0}; + HDC hDC = BeginPaint(hWnd, &ps); + + RECT rc; + GetClientRect(hWnd, &rc); + rc.left = *(int*)GetProp(hWnd, TEXT("SPLITTERWIDTH")); + rc.right = rc.left + 5; + FillRect(hDC, &rc, (HBRUSH)GetProp(hWnd, TEXT("GRAYBRUSH"))); + EndPaint(hWnd, &ps); + + return 0; + } + break; + + // https://groups.google.com/g/comp.os.ms-windows.programmer.win32/c/1XhCKATRXws + case WM_NCHITTEST: { + return 1; + } + break; + + case WM_LBUTTONDOWN: { + SetProp(hWnd, TEXT("ISMOUSEDOWN"), (HANDLE)1); + SetCapture(hWnd); + return 0; + } + break; + + case WM_LBUTTONUP: { + ReleaseCapture(); + RemoveProp(hWnd, TEXT("ISMOUSEDOWN")); + } + break; + + case WM_MOUSEMOVE: { + if (wParam != MK_LBUTTON || !GetProp(hWnd, TEXT("ISMOUSEDOWN"))) + return 0; + + DWORD x = GET_X_LPARAM(lParam); + if (x > 0 && x < 32000) + *(int*)GetProp(hWnd, TEXT("SPLITTERWIDTH")) = x; + SendMessage(hWnd, WM_SIZE, 0, 0); + } + break; + + case WM_MOUSEWHEEL: { + if (LOWORD(wParam) == MK_CONTROL) { + SendMessage(hWnd, WMU_SET_FONT, GET_WHEEL_DELTA_WPARAM(wParam) > 0 ? 1: -1, 0); + return 1; + } + } + break; + + case WM_CONTEXTMENU: { + POINT p = {LOWORD(lParam), HIWORD(lParam)}; + if (GetDlgCtrlID(WindowFromPoint(p)) == IDC_TEXT) + TrackPopupMenu(GetProp(hWnd, TEXT("TEXTMENU")), TPM_RIGHTBUTTON | TPM_TOPALIGN | TPM_LEFTALIGN, p.x, p.y, 0, hWnd, NULL); + } + break; + + case WM_COMMAND: { + WORD cmd = LOWORD(wParam); + if (cmd == IDM_COPY_CELL || cmd == IDM_COPY_ROW) { + HWND hTabWnd = GetDlgItem(hWnd, IDC_TAB); + HWND hGridWnd = GetDlgItem(hTabWnd, IDC_GRID); + HWND hHeader = ListView_GetHeader(hGridWnd); + int rowNo = ListView_GetNextItem(hGridWnd, -1, LVNI_SELECTED); + if (rowNo != -1) { + TCHAR*** cache = (TCHAR***)GetProp(hWnd, TEXT("CACHE")); + int* resultset = (int*)GetProp(hWnd, TEXT("RESULTSET")); + int rowCount = *(int*)GetProp(hWnd, TEXT("ROWCOUNT")); + if (!resultset || rowNo >= rowCount) + return 0; + + int colCount = Header_GetItemCount(hHeader); + + int startNo = cmd == IDM_COPY_CELL ? *(int*)GetProp(hWnd, TEXT("COLNO")) : 0; + int endNo = cmd == IDM_COPY_CELL ? startNo + 1 : colCount; + if (startNo > colCount || endNo > colCount) + return 0; + + int len = 0; + for (int colNo = startNo; colNo < endNo; colNo++) { + int _rowNo = resultset[rowNo]; + len += _tcslen(cache[_rowNo][colNo]) + 1 /* column delimiter: TAB */; + } + + TCHAR* buf = calloc(len + 1, sizeof(TCHAR)); + for (int colNo = startNo; colNo < endNo; colNo++) { + int _rowNo = resultset[rowNo]; + _tcscat(buf, cache[_rowNo][colNo]); + if (colNo != endNo - 1) + _tcscat(buf, TEXT("\t")); + } + + setClipboardText(buf); + free(buf); + } + } + + if (cmd == IDM_COPY_TEXT || cmd == IDM_SELECTALL) { + HWND hTabWnd = GetDlgItem(hWnd, IDC_TAB); + HWND hTextWnd = GetDlgItem(hTabWnd, IDC_TEXT); + SendMessage(hTextWnd, cmd == IDM_COPY_TEXT ? WM_COPY : EM_SETSEL, 0, -1); + } + + if (cmd == IDM_FORMAT) { + HMENU hMenu = (HMENU)GetProp(hWnd, TEXT("TEXTMENU")); + int* pIsFormat = (int*)GetProp(hWnd, TEXT("ISFORMAT")); + *pIsFormat = (*pIsFormat + 1) % 2; + + MENUITEMINFO mii = {0}; + mii.cbSize = sizeof(MENUITEMINFO); + mii.fMask = MIIM_STATE; + mii.fState = *pIsFormat ? MFS_CHECKED : 0; + SetMenuItemInfo(hMenu, IDM_FORMAT, FALSE, &mii); + + SendMessage(hWnd, WMU_UPDATE_TEXT, 0, 0); + } + } + break; + + case WM_NOTIFY : { + NMHDR* pHdr = (LPNMHDR)lParam; + if (pHdr->idFrom == IDC_TAB && pHdr->code == TCN_SELCHANGE) { + HWND hTabWnd = pHdr->hwndFrom; + BOOL isText = TabCtrl_GetCurSel(hTabWnd) == 1; + ShowWindow(GetDlgItem(hTabWnd, IDC_GRID), isText ? SW_HIDE : SW_SHOW); + ShowWindow(GetDlgItem(hTabWnd, IDC_TEXT), isText ? SW_SHOW : SW_HIDE); + } + + if (pHdr->idFrom == IDC_TREE && pHdr->code == TVN_SELCHANGED) { + SendMessage(hWnd, WMU_UPDATE_GRID, 0, 0); + SendMessage(hWnd, WMU_UPDATE_TEXT, 0, 0); + } + + if (pHdr->idFrom == IDC_TREE && pHdr->code == TVN_ITEMEXPANDING) { + HWND hTreeWnd = pHdr->hwndFrom; + NMTREEVIEW* tv = (NMTREEVIEW*) lParam; + HTREEITEM hItem = tv->itemNew.hItem; + HTREEITEM hChild = TreeView_GetChild(hTreeWnd, hItem); + + if (hChild) { + TCHAR nodeName[256]; + TreeView_GetItemText(hTreeWnd, hChild, nodeName, 255); + if (_tcscmp(nodeName, LOADING) == 0) { + TreeView_DeleteItem(hTreeWnd, hChild); + + xml_element* node = (xml_element*)TreeView_GetItemParam(hTreeWnd, hItem); + xml_element* subnode = node != NULL ? node->first_child : 0; + while (subnode) { + addNode(hTreeWnd, hItem, subnode); + subnode = subnode->next; + } + } + } + } + + if (pHdr->idFrom == IDC_GRID && pHdr->code == LVN_GETDISPINFO) { + LV_DISPINFO* pDispInfo = (LV_DISPINFO*)lParam; + LV_ITEM* pItem = &(pDispInfo)->item; + + TCHAR*** cache = (TCHAR***)GetProp(hWnd, TEXT("CACHE")); + int* resultset = (int*)GetProp(hWnd, TEXT("RESULTSET")); + if(resultset && pItem->mask & LVIF_TEXT) { + int rowNo = resultset[pItem->iItem]; + pItem->pszText = cache[rowNo][pItem->iSubItem]; + } + } + + if (pHdr->idFrom == IDC_GRID && pHdr->code == LVN_COLUMNCLICK) { + NMLISTVIEW* pLV = (NMLISTVIEW*)lParam; + int colNo = pLV->iSubItem + 1; + int* pOrderBy = (int*)GetProp(hWnd, TEXT("ORDERBY")); + int orderBy = *pOrderBy; + *pOrderBy = colNo == orderBy || colNo == -orderBy ? -orderBy : colNo; + SendMessage(hWnd, WMU_UPDATE_RESULTSET, 0, 0); + } + + if (pHdr->idFrom == IDC_GRID && pHdr->code == (DWORD)NM_RCLICK) { + NMITEMACTIVATE* ia = (LPNMITEMACTIVATE) lParam; + POINT p; + GetCursorPos(&p); + *(int*)GetProp(hWnd, TEXT("COLNO")) = ia->iSubItem; + TrackPopupMenu(GetProp(hWnd, TEXT("DATAMENU")), TPM_RIGHTBUTTON | TPM_TOPALIGN | TPM_LEFTALIGN, p.x, p.y, 0, hWnd, NULL); + } + + if (pHdr->idFrom == IDC_GRID && pHdr->code == (DWORD)LVN_ITEMCHANGED) { + HWND hStatusWnd = GetDlgItem(hWnd, IDC_STATUSBAR); + + TCHAR buf[255] = {0}; + int pos = ListView_GetNextItem(pHdr->hwndFrom, -1, LVNI_SELECTED); + if (pos != -1) + _sntprintf(buf, 255, TEXT(" %i"), pos + 1); + SendMessage(hStatusWnd, SB_SETTEXT, SB_CURRENT_ROW, (LPARAM)buf); + } + + if (pHdr->idFrom == IDC_GRID && pHdr->code == (DWORD)LVN_KEYDOWN) { + NMLVKEYDOWN* kd = (LPNMLVKEYDOWN) lParam; + if (kd->wVKey == 0x43 && GetKeyState(VK_CONTROL)) // Ctrl + C + SendMessage(hWnd, WM_COMMAND, IDM_COPY_ROW, 0); + } + + if (pHdr->code == HDN_ITEMCHANGED && pHdr->hwndFrom == ListView_GetHeader(GetDlgItem(GetDlgItem(hWnd, IDC_TAB), IDC_GRID))) + SendMessage(hWnd, WMU_UPDATE_FILTER_SIZE, 0, 0); + } + break; + + case WMU_UPDATE_GRID: { + HWND hTreeWnd = GetDlgItem(hWnd, IDC_TREE); + HWND hTabWnd = GetDlgItem(hWnd, IDC_TAB); + HWND hGridWnd = GetDlgItem(hTabWnd, IDC_GRID); + HWND hStatusWnd = GetDlgItem(hWnd, IDC_STATUSBAR); + + HTREEITEM hItem = TreeView_GetSelection(hTreeWnd); + xml_element* node = (xml_element*)TreeView_GetItemParam(hTreeWnd, hItem); + + HWND hHeader = ListView_GetHeader(hGridWnd); + SendMessage(hWnd, WMU_RESET_CACHE, 0, 0); + *(int*)GetProp(hWnd, TEXT("ORDERBY")) = 0; + + int colCount = Header_GetItemCount(hHeader); + for (int colNo = 0; colNo < colCount; colNo++) + DestroyWindow(GetDlgItem(hHeader, IDC_HEADER_EDIT + colNo)); + + for (int colNo = 0; colNo < colCount; colNo++) + ListView_DeleteColumn(hGridWnd, colCount - colNo - 1); + + BOOL isTable = TRUE; + xml_element* template = 0; + int childCount = 0; + + xml_element* subnode = node->first_child; + char* tagName = 0; + while (isTable && subnode) { + childCount += subnode->key != 0; + if (tagName && subnode->key) + isTable = strcmp(subnode->key, tagName) == 0; + + if (!tagName && subnode->key && strlen(subnode->key) > 0) { + tagName = subnode->key; + template = subnode; + } + + subnode = subnode->next; + } + isTable = isTable && childCount > 1; + + if (isTable) { + xml_attribute* attr = template->first_attribute; + while (attr) { + char* colName8 = calloc(strlen(attr->key) + 2, sizeof(char)); + sprintf(colName8, "@%s", attr->key); + TCHAR* colName16 = utf8to16(colName8); + ListView_AddColumn(hGridWnd, colName16); + free(colName16); + free(colName8); + + attr = attr->next; + } + + subnode = template->first_child; + while (subnode) { + if (subnode->key && strlen(subnode->key) > 0) { + + TCHAR* colName16 = utf8to16(subnode->key); + ListView_AddColumn(hGridWnd, colName16); + free(colName16); + } + subnode = subnode->next; + } + + ListView_AddColumn(hGridWnd, TEXT("#CONTENT")); + } else { + ListView_AddColumn(hGridWnd, TEXT("Key")); + ListView_AddColumn(hGridWnd, TEXT("Value")); + } + + colCount = Header_GetItemCount(hHeader); + for (int colNo = 0; colNo < colCount; colNo++) { + // Use WS_BORDER to vertical text aligment + HWND hEdit = CreateWindowEx(WS_EX_TOPMOST, WC_EDIT, NULL, ES_CENTER | ES_AUTOHSCROLL | WS_VISIBLE | WS_CHILD | WS_TABSTOP | WS_BORDER, + 0, 0, 0, 0, hHeader, (HMENU)(INT_PTR)(IDC_HEADER_EDIT + colNo), GetModuleHandle(0), NULL); + SendMessage(hEdit, WM_SETFONT, (LPARAM)GetProp(hWnd, TEXT("FONT")), TRUE); + SetProp(hEdit, TEXT("WNDPROC"), (HANDLE)SetWindowLongPtr(hEdit, GWLP_WNDPROC, (LONG_PTR)cbNewFilterEdit)); + } + + + // Cache data + int* pTotalRowCount = (int*)GetProp(hWnd, TEXT("TOTALROWCOUNT")); + ListView_SetItemCount(hGridWnd, 0); + SetProp(hWnd, TEXT("CACHE"), 0); + + TCHAR*** cache = 0; + int rowCount = 0; + int rowNo = 0; + if (isTable) { + rowCount = node->child_count; + cache = calloc(rowCount, sizeof(TCHAR*)); + + xml_element* subnode = node->first_child; + while (subnode) { + if (!subnode->key) { + subnode = subnode->next; + continue; + } + + cache[rowNo] = (TCHAR**)calloc (colCount, sizeof (TCHAR*)); + int colNo = 0; + + xml_attribute* attr = template->first_attribute; + while (attr) { + xml_attribute* sa = xml_find_attribute(subnode, attr->key); + cache[rowNo][colNo] = utf8to16(sa ? sa->value : "N/A"); + + colNo++; + attr = attr->next; + } + + xml_element* tagNode = template->first_child; + while (tagNode) { + if (!tagNode->key) { + tagNode = tagNode->next; + continue; + } + + xml_element* sn = xml_find_element(subnode, tagNode->key); + cache[rowNo][colNo] = utf8to16(sn ? xml_content(sn) : "N/A"); + + colNo++; + tagNode = tagNode->next; + } + cache[rowNo][colNo] = utf8to16(subnode ? xml_content(subnode) : ""); + + rowNo++; + subnode = subnode->next; + } + } else { + rowCount = node->attribute_count + node->child_count + 1; + cache = calloc(rowCount, sizeof(TCHAR*)); + + xml_attribute* attr = node->first_attribute; + while (attr) { + cache[rowNo] = (TCHAR**)calloc (colCount, sizeof (TCHAR*)); + char* buf8 = calloc(strlen(attr->key) + 2, sizeof(char)); + sprintf(buf8, "@%s", attr->key); + cache[rowNo][0] = utf8to16(buf8); + free(buf8); + cache[rowNo][1] = utf8to16(attr->value); + + rowNo++; + attr = attr->next; + } + + xml_element* subnode = node->first_child; + while (subnode) { + if (!subnode->key && isEmpty(subnode->value)){ + subnode = subnode->next; + continue; + } + + cache[rowNo] = (TCHAR**)calloc (colCount, sizeof (TCHAR*)); + if (subnode->key) { + if (strncmp(subnode->key, "![CDATA[", 8) == 0) { + int len = strlen(subnode->key); + char* buf = calloc(len + 1, sizeof(char)); + sprintf(buf, "%.*s", len - strlen("![CDATA[]]>"), subnode->key + 8); + cache[rowNo][0] = utf8to16("[CDATA]"); + cache[rowNo][1] = utf8to16(buf); + free(buf); + } else if (strncmp(subnode->key, "!--", 3) == 0) { + int len = strlen(subnode->key); + char* buf = calloc(len + 1, sizeof(char)); + sprintf(buf, "%.*s", len - strlen("!----"), subnode->key + 3); + cache[rowNo][0] = utf8to16("[COMMENT]"); + cache[rowNo][1] = utf8to16(buf); + free(buf); + } else { + cache[rowNo][0] = utf8to16(subnode->key); + cache[rowNo][1] = utf8to16(xml_content(subnode)); + } + } else { + cache[rowNo][0] = utf8to16("[TEXT]"); + cache[rowNo][1] = utf8to16(subnode->value); + } + + rowNo++; + subnode = subnode->next; + } + } + + if (rowNo == 0) { + if (cache) + free(cache); + } else { + cache = realloc(cache, rowNo * sizeof(TCHAR*)); + SetProp(hWnd, TEXT("CACHE"), cache); + } + + SendMessage(hStatusWnd, SB_SETTEXT, SB_MODE, (LPARAM)(isTable ? TEXT(" TABLE") : TEXT(" SINGLE"))); + + *pTotalRowCount = rowNo; + SendMessage(hWnd, WMU_UPDATE_RESULTSET, 0, 0); + SendMessage(hWnd, WMU_AUTO_COLUMN_SIZE, 0, 0); + } + break; + + case WMU_UPDATE_RESULTSET: { + HWND hTabWnd = GetDlgItem(hWnd, IDC_TAB); + HWND hGridWnd = GetDlgItem(hTabWnd, IDC_GRID); + HWND hStatusWnd = GetDlgItem(hWnd, IDC_STATUSBAR); + HWND hHeader = ListView_GetHeader(hGridWnd); + + ListView_SetItemCount(hGridWnd, 0); + TCHAR*** cache = (TCHAR***)GetProp(hWnd, TEXT("CACHE")); + int* pTotalRowCount = (int*)GetProp(hWnd, TEXT("TOTALROWCOUNT")); + int* pRowCount = (int*)GetProp(hWnd, TEXT("ROWCOUNT")); + int* pOrderBy = (int*)GetProp(hWnd, TEXT("ORDERBY")); + int* resultset = (int*)GetProp(hWnd, TEXT("RESULTSET")); + if (resultset) + free(resultset); + + if (!cache) + return 1; + + if (*pTotalRowCount == 0) + return 1; + + int colCount = Header_GetItemCount(hHeader); + if (colCount == 0) + return 1; + + BOOL* bResultset = (BOOL*)calloc(*pTotalRowCount, sizeof(BOOL)); + for (int rowNo = 0; rowNo < *pTotalRowCount; rowNo++) + bResultset[rowNo] = TRUE; + + for (int colNo = 0; colNo < colCount; colNo++) { + HWND hEdit = GetDlgItem(hHeader, IDC_HEADER_EDIT + colNo); + TCHAR filter[MAX_LENGTH]; + GetWindowText(hEdit, filter, MAX_LENGTH); + int len = _tcslen(filter); + if (len == 0) + continue; + + for (int rowNo = 0; rowNo < *pTotalRowCount; rowNo++) { + if (!bResultset[rowNo]) + continue; + + TCHAR* value = cache[rowNo][colNo]; + if (len > 1 && (filter[0] == TEXT('<') || filter[0] == TEXT('>')) && isNumber(filter + 1)) { + TCHAR* end = 0; + double df = _tcstod(filter + 1, &end); + double dv = _tcstod(value, &end); + bResultset[rowNo] = (filter[0] == TEXT('<') && dv < df) || (filter[0] == TEXT('>') && dv > df); + } else { + bResultset[rowNo] = len == 1 ? _tcsstr(value, filter) != 0 : + filter[0] == TEXT('=') ? _tcscmp(value, filter + 1) == 0 : + filter[0] == TEXT('!') ? _tcsstr(value, filter + 1) == 0 : + filter[0] == TEXT('<') ? _tcscmp(value, filter + 1) < 0 : + filter[0] == TEXT('>') ? _tcscmp(value, filter + 1) > 0 : + _tcsstr(value, filter) != 0; + } + } + } + + int rowCount = 0; + resultset = (int*)calloc(*pTotalRowCount, sizeof(int)); + for (int rowNo = 0; rowNo < *pTotalRowCount; rowNo++) { + if (!bResultset[rowNo]) + continue; + + resultset[rowCount] = rowNo; + rowCount++; + } + free(bResultset); + + if (rowCount > 0) { + if (rowCount > *pTotalRowCount) + MessageBeep(0); + resultset = realloc(resultset, rowCount * sizeof(int)); + SetProp(hWnd, TEXT("RESULTSET"), (HANDLE)resultset); + int orderBy = *pOrderBy; + + if (orderBy) { + // Bubble-sort + for (int i = 0; i < rowCount; i++) { + for (int j = i + 1; j < rowCount; j++) { + int a = resultset[i]; + int b = resultset[j]; + + if(orderBy > 0 && _tcscmp(cache[a][orderBy - 1], cache[b][orderBy - 1]) > 0) { + resultset[i] = b; + resultset[j] = a; + } + + if(orderBy < 0 && _tcscmp(cache[a][-orderBy - 1], cache[b][-orderBy - 1]) < 0) { + resultset[i] = b; + resultset[j] = a; + } + } + } + } + } else { + SetProp(hWnd, TEXT("RESULTSET"), (HANDLE)0); + free(resultset); + } + + *pRowCount = rowCount; + ListView_SetItemCount(hGridWnd, rowCount); + InvalidateRect(hGridWnd, NULL, TRUE); + + TCHAR buf[255]; + _sntprintf(buf, 255, TEXT(" Rows: %i/%i"), rowCount, *pTotalRowCount); + SendMessage(hStatusWnd, SB_SETTEXT, SB_ROW_COUNT, (LPARAM)buf); + + PostMessage(hWnd, WMU_UPDATE_FILTER_SIZE, 0, 0); + } + break; + + case WMU_UPDATE_TEXT: { + HWND hTreeWnd = GetDlgItem(hWnd, IDC_TREE); + HWND hTabWnd = GetDlgItem(hWnd, IDC_TAB); + HWND hTextWnd = GetDlgItem(hTabWnd, IDC_TEXT); + char* data = (char*)GetProp(hWnd, TEXT("DATA")); + + HTREEITEM hItem = TreeView_GetSelection(hTreeWnd); + xml_element* node = (xml_element*)TreeView_GetItemParam(hTreeWnd, hItem); + char* text8 = calloc(node->length + 1, sizeof(char)); + strncpy(text8, data + node->offset, node->length); + + if (*(int*)GetProp(hWnd, TEXT("ISFORMAT"))) { + char* ftext8 = formatXML(text8); + free(text8); + text8 = ftext8; + } + TCHAR* text16 = utf8to16(text8); + free(text8); + + LockWindowUpdate(hTextWnd); + SendMessage(hTextWnd, EM_EXLIMITTEXT, 0, _tcslen(text16) + 1); + SetWindowText(hTextWnd, text16); + LockWindowUpdate(0); + free(text16); + + SendMessage(hWnd, WMU_UPDATE_HIGHLIGHT, 0, 0); + } + break; + + case WMU_UPDATE_HIGHLIGHT: { + HWND hTabWnd = GetDlgItem(hWnd, IDC_TAB); + HWND hTextWnd = GetDlgItem(hTabWnd, IDC_TEXT); + + GETTEXTLENGTHEX gtl = {GTL_NUMBYTES, 0}; + int len = SendMessage(hTextWnd, EM_GETTEXTLENGTHEX, (WPARAM)>l, 1200); + if (len < *(int*)GetProp(hWnd, TEXT("MAXHIGHLIGHTLENGTH"))) { + TCHAR* text = calloc(len + sizeof(TCHAR), sizeof(char)); + GETTEXTEX gt = {0}; + gt.cb = len + sizeof(TCHAR); + gt.flags = 0; + gt.codepage = 1200; + SendMessage(hTextWnd, EM_GETTEXTEX, (WPARAM)>, (LPARAM)text); + LockWindowUpdate(hTextWnd); + highlightText(hTextWnd, text); + free(text); + SendMessage(hTextWnd, EM_SETSEL, 0, 0); + LockWindowUpdate(0); + InvalidateRect(hTextWnd, NULL, TRUE); + } + } + break; + + case WMU_UPDATE_FILTER_SIZE: { + HWND hTabWnd = GetDlgItem(hWnd, IDC_TAB); + HWND hGridWnd = GetDlgItem(hTabWnd, IDC_GRID); + HWND hHeader = ListView_GetHeader(hGridWnd); + int colCount = Header_GetItemCount(hHeader); + SendMessage(hHeader, WM_SIZE, 0, 0); + for (int colNo = 0; colNo < colCount; colNo++) { + RECT rc; + Header_GetItemRect(hHeader, colNo, &rc); + int h2 = round((rc.bottom - rc.top) / 2); + SetWindowPos(GetDlgItem(hHeader, IDC_HEADER_EDIT + colNo), 0, rc.left - (colNo > 0), h2, rc.right - rc.left + 1, h2 + 1, SWP_NOZORDER); + } + } + break; + + case WMU_AUTO_COLUMN_SIZE: { + HWND hTabWnd = GetDlgItem(hWnd, IDC_TAB); + HWND hGridWnd = GetDlgItem(hTabWnd, IDC_GRID); + SendMessage(hGridWnd, WM_SETREDRAW, FALSE, 0); + HWND hHeader = ListView_GetHeader(hGridWnd); + int colCount = Header_GetItemCount(hHeader); + + for (int colNo = 0; colNo < colCount - 1; colNo++) + ListView_SetColumnWidth(hGridWnd, colNo, colNo < colCount - 1 ? LVSCW_AUTOSIZE_USEHEADER : LVSCW_AUTOSIZE); + + if (colCount == 1 && ListView_GetColumnWidth(hGridWnd, 0) < 100) + ListView_SetColumnWidth(hGridWnd, 0, 100); + + int maxWidth = getStoredValue(TEXT("max-column-width"), 300); + if (colCount > 1) { + for (int colNo = 0; colNo < colCount; colNo++) { + if (ListView_GetColumnWidth(hGridWnd, colNo) > maxWidth) + ListView_SetColumnWidth(hGridWnd, colNo, maxWidth); + } + } + + // Fix last column + if (colCount > 1) { + int colNo = colCount - 1; + ListView_SetColumnWidth(hGridWnd, colNo, LVSCW_AUTOSIZE); + TCHAR name16[MAX_LENGTH + 1]; + Header_GetItemText(hHeader, colNo, name16, MAX_LENGTH); + + SIZE s = {0}; + HDC hDC = GetDC(hHeader); + HFONT hOldFont = (HFONT)SelectObject(hDC, (HFONT)GetProp(hWnd, TEXT("FONT"))); + GetTextExtentPoint32(hDC, name16, _tcslen(name16), &s); + SelectObject(hDC, hOldFont); + ReleaseDC(hHeader, hDC); + + int w = s.cx + 12; + if (ListView_GetColumnWidth(hGridWnd, colNo) < w) + ListView_SetColumnWidth(hGridWnd, colNo, w); + + if (ListView_GetColumnWidth(hGridWnd, colNo) > maxWidth) + ListView_SetColumnWidth(hGridWnd, colNo, maxWidth); + } + + SendMessage(hGridWnd, WM_SETREDRAW, TRUE, 0); + InvalidateRect(hGridWnd, NULL, TRUE); + + PostMessage(hWnd, WMU_UPDATE_FILTER_SIZE, 0, 0); + } + break; + + case WMU_RESET_CACHE: { + HWND hTabWnd = GetDlgItem(hWnd, IDC_TAB); + HWND hGridWnd = GetDlgItem(hTabWnd, IDC_GRID); + TCHAR*** cache = (TCHAR***)GetProp(hWnd, TEXT("CACHE")); + int* pTotalRowCount = (int*)GetProp(hWnd, TEXT("TOTALROWCOUNT")); + + int colCount = Header_GetItemCount(ListView_GetHeader(hGridWnd)); + if (colCount > 0 && cache != 0) { + for (int rowNo = 0; rowNo < *pTotalRowCount; rowNo++) { + if (cache[rowNo]) { + for (int colNo = 0; colNo < colCount; colNo++) + if (cache[rowNo][colNo]) + free(cache[rowNo][colNo]); + + free(cache[rowNo]); + } + cache[rowNo] = 0; + } + free(cache); + } + + int* resultset = (int*)GetProp(hWnd, TEXT("RESULTSET")); + if (resultset) + free(resultset); + SetProp(hWnd, TEXT("RESULTSET"), 0); + + int* pRowCount = (int*)GetProp(hWnd, TEXT("ROWCOUNT")); + *pRowCount = 0; + + SetProp(hWnd, TEXT("CACHE"), 0); + *pTotalRowCount = 0; + } + break; + + // wParam - size delta + case WMU_SET_FONT: { + int* pFontSize = (int*)GetProp(hWnd, TEXT("FONTSIZE")); + if (*pFontSize + wParam < 10 || *pFontSize + wParam > 48) + return 0; + *pFontSize += wParam; + DeleteFont(GetProp(hWnd, TEXT("FONT"))); + + HFONT hFont = CreateFont (*pFontSize, 0, 0, 0, FW_DONTCARE, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, TEXT("Arial")); + HWND hTreeWnd = GetDlgItem(hWnd, IDC_TREE); + HWND hTabWnd = GetDlgItem(hWnd, IDC_TAB); + HWND hGridWnd = GetDlgItem(hTabWnd, IDC_GRID); + HWND hTextWnd = GetDlgItem(hTabWnd, IDC_TEXT); + SendMessage(hTreeWnd, WM_SETFONT, (LPARAM)hFont, TRUE); + SendMessage(hGridWnd, WM_SETFONT, (LPARAM)hFont, TRUE); + SendMessage(hTextWnd, WM_SETFONT, (LPARAM)hFont, TRUE); + SendMessage(hTabWnd, WM_SETFONT, (LPARAM)GetStockObject(DEFAULT_GUI_FONT), TRUE); + + HWND hHeader = ListView_GetHeader(hGridWnd); + for (int colNo = 0; colNo < Header_GetItemCount(hHeader); colNo++) + SendMessage(GetDlgItem(hHeader, IDC_HEADER_EDIT + colNo), WM_SETFONT, (LPARAM)hFont, TRUE); + + SetProp(hWnd, TEXT("FONT"), hFont); + + PostMessage(hWnd, WMU_UPDATE_HIGHLIGHT, 0, 0); + PostMessage(hWnd, WMU_AUTO_COLUMN_SIZE, 0, 0); + } + break; + + } + return CallWindowProc((WNDPROC)GetProp(hWnd, TEXT("WNDPROC")), hWnd, msg, wParam, lParam); +} + +LRESULT CALLBACK cbNewFilterEdit(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { + WNDPROC cbDefault = (WNDPROC)GetProp(hWnd, TEXT("WNDPROC")); + + switch(msg){ + // Win10+ fix: draw an upper border + case WM_PAINT: { + cbDefault(hWnd, msg, wParam, lParam); + + RECT rc; + GetWindowRect(hWnd, &rc); + HDC hDC = GetWindowDC(hWnd); + HPEN hPen = CreatePen(PS_SOLID, 1, GetSysColor(COLOR_BTNFACE)); + HPEN oldPen = SelectObject(hDC, hPen); + MoveToEx(hDC, 1, 0, 0); + LineTo(hDC, rc.right - 1, 0); + SelectObject(hDC, oldPen); + DeleteObject(hPen); + ReleaseDC(hWnd, hDC); + + return 0; + } + break; + + // Prevent beep + case WM_CHAR: { + if (wParam == VK_RETURN || wParam == VK_ESCAPE || wParam == VK_TAB) + return 0; + } + break; + + case WM_KEYDOWN: { + if (wParam == VK_RETURN || wParam == VK_ESCAPE || wParam == VK_TAB) { + if (wParam == VK_RETURN) { + HWND hHeader = GetParent(hWnd); + HWND hGridWnd = GetParent(hHeader); + HWND hTabWnd = GetParent(hGridWnd); + HWND hMainWnd = GetParent(hTabWnd); + SendMessage(hMainWnd, WMU_UPDATE_RESULTSET, 0, 0); + } + + return 0; + } + } + break; + + case WM_DESTROY: { + RemoveProp(hWnd, TEXT("WNDPROC")); + } + break; + } + + return CallWindowProc(cbDefault, hWnd, msg, wParam, lParam); +} + +LRESULT CALLBACK cbNewText(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { + if (msg == EM_SETZOOM) + return 0; + + if (msg == WM_MOUSEWHEEL && LOWORD(wParam) == MK_CONTROL) { + HWND hTabWnd = GetParent(hWnd); + SendMessage(GetParent(hTabWnd), msg, wParam, lParam); + return 0; + } + + return CallWindowProc((WNDPROC)GetProp(hWnd, TEXT("WNDPROC")), hWnd, msg, wParam, lParam); +} + +HTREEITEM addNode(HWND hTreeWnd, HTREEITEM hParentItem, xml_element* node) { + if (!node) + return FALSE; + + HTREEITEM hItem = 0; + xml_element* subnode = node->first_child; + if (node->key) { + const char* value = node->value ? node->value : subnode && !subnode->key ? subnode->value : 0; + char* buf8 = calloc(strlen(node->key) + (value ? strlen(value) : 0) + 10, sizeof(char)); + if (node->key && strncmp(node->key, "![CDATA[", 8) == 0) { + sprintf(buf8, "#CDATA (%.2fKb)", strlen(node->key)/1024.0); + } else if (node->key && strncmp(node->key, "!--", 3) == 0) { + sprintf(buf8, "#COMMENT"); + } else if (!isEmpty(value)) { + sprintf(buf8, "%s = %s", node->key, value); + } else { + sprintf(buf8, "%s", node->key); + } + + TCHAR* name16 = utf8to16(buf8); + hItem = TreeView_AddItem(hTreeWnd, name16, hParentItem, (LPARAM)node); + free(name16); + free(buf8); + } + + BOOL hasSubnode = FALSE; + while (!hasSubnode && subnode) { + hasSubnode = subnode->key != NULL; + subnode = subnode->next; + } + if (hasSubnode) + TreeView_AddItem(hTreeWnd, LOADING, hItem, (LPARAM)subnode); + + return hItem; +} + +void highlightText (HWND hWnd, TCHAR* text) { + BOOL inTag = FALSE; + TCHAR q = 0; + BOOL isValue = FALSE; + + CHARFORMAT2 cf2 = {0}; + cf2.cbSize = sizeof(CHARFORMAT2) ; + cf2.dwMask = CFM_COLOR | CFM_BOLD; + + int len = _tcslen(text); + int pos = 0; + while (pos < len) { + int start = pos; + TCHAR c = text[pos]; + + cf2.crTextColor = RGB(0, 0, 0); + cf2.dwEffects = 0; + + if (c == TEXT('\'') || c == TEXT('"')) { + TCHAR* p = _tcschr(text + pos + 1, c); + if (p != NULL) { + pos = p - text; + cf2.crTextColor = RGB(0, 200, 0); + } + } else if (c == TEXT('<') && _tcsncmp(text + pos, TEXT("")); + if (p != NULL) { + pos = p - text + 2; + cf2.crTextColor = RGB(196, 196, 196); + } + } else if (c == TEXT('<') && _tcsncmp(text + pos, TEXT("")); + if (p != NULL) { + pos = p - text + 2; + cf2.crTextColor = RGB(126, 0, 0); + } + } else if (c == TEXT('<')) { + TCHAR* p = _tcspbrk(text + pos, TEXT(" \t\r\n>")); + if (p != NULL) { + pos = p - text; + cf2.crTextColor = RGB(0, 0, 128); + cf2.dwEffects = CFM_BOLD; + } + } else if (c == TEXT('>')) { + if (pos > 0 && text[pos - 1] == TEXT('/')) + start--; + + cf2.crTextColor = RGB(0, 0, 128); + cf2.dwEffects = CFM_BOLD; + } else if (pos > 0 && text[pos - 1] == TEXT('>')) { + TCHAR* p = _tcschr(text + pos + 1, TEXT('<')); + if (p != NULL) { + pos = p - text - 1; + cf2.crTextColor = RGB(255, 128, 0); + } + } + + SendMessage(hWnd, EM_SETSEL, start, pos + 1); + SendMessage(hWnd, EM_SETCHARFORMAT, SCF_SELECTION, (LPARAM) &cf2); + pos++; + } +} + +char* formatXML(const char* data) { + if (!data) + return 0; + + int len = strlen(data); + int bLen = len; + char* buf = calloc(bLen, sizeof(char)); + + BOOL inTag = FALSE; + BOOL isAttr = FALSE; + BOOL isValue = FALSE; + char quote = 0; + BOOL isOpenTag = FALSE; + + int bPos = 0; + int pos = 0; + int level = 0; + while (pos < len) { + char c = data[pos]; + char p = bPos > 0 ? buf[bPos - 1] : 0; + char n = data[pos + 1]; + + BOOL isSave = FALSE; + BOOL isSpace = FALSE; + BOOL isNun = strchr(" \t\r\n", c) != 0; + + if (1000 == 1000 && quote && c != quote) { + isSave = TRUE; + } else if (quote && c == quote) { + isSave = TRUE; + quote = 0; + } else if (c == '<') { + inTag = TRUE; + isSave = TRUE; + isValue = FALSE; + isOpenTag = n != '/'; + if (!isOpenTag) + level--; + } else if (inTag && c == '>') { + isSave = TRUE; + inTag = FALSE; + isAttr = FALSE; + isValue = FALSE; + if (p == '/' || p == '-') + level--; + quote = 0; + } else if (inTag && !isNun) { + isSave = TRUE; + isSpace = !isAttr && p != '<' && p != '/'; + isAttr = TRUE; + } else if (inTag && isNun) { + isAttr = FALSE; + } else if (!inTag && p == '>'){ + isValue = FALSE; + isSave = !isNun; + } else if (!inTag && !isValue && !isNun) { + isSave = TRUE; + isValue = TRUE; + } else if (!inTag && isValue && !isNun) { + isSave = TRUE; + } else if (!inTag && isValue && isNun) { + int n = strspn(data + pos, " \t\r\n"); + BOOL isEmptyTail = data[pos + n] == '<'; + if (isEmptyTail) { + isValue = FALSE; + } else { + isSave = TRUE; + } + } + + if (isSave && bPos > 0 && (c == '<' || p == '>')) { + buf[bPos] = '\n'; + bPos++; + + for (int l = 0; l < level; l++) { + buf[bPos] = '\t'; + bPos++; + } + } + + if (isSpace) { + buf[bPos] = ' '; + bPos++; + } + + if (isSave) { + buf[bPos] = data[pos]; + bPos++; + + if (bLen - bPos < 100) { + bLen += 32000; + buf = realloc(buf, bLen); + } + } + + if (c == '<' && isOpenTag) + level++; + + pos++; + } + buf[bPos] = 0; + + return buf; +} + +void setStoredValue(TCHAR* name, int value) { + TCHAR buf[128]; + _sntprintf(buf, 128, TEXT("%i"), value); + WritePrivateProfileString(APP_NAME, name, buf, iniPath); +} + +int getStoredValue(TCHAR* name, int defValue) { + TCHAR buf[128]; + return GetPrivateProfileString(APP_NAME, name, NULL, buf, 128, iniPath) ? _ttoi(buf) : defValue; +} + +int CALLBACK cbEnumTabStopChildren (HWND hWnd, LPARAM lParam) { + if (GetWindowLong(hWnd, GWL_STYLE) & WS_TABSTOP && IsWindowVisible(hWnd)) { + int no = 0; + HWND* wnds = (HWND*)lParam; + while (wnds[no]) + no++; + wnds[no] = hWnd; + } + + return TRUE; +} + +TCHAR* utf8to16(const char* in) { + TCHAR *out; + if (!in || strlen(in) == 0) { + out = (TCHAR*)calloc (1, sizeof (TCHAR)); + } else { + DWORD size = MultiByteToWideChar(CP_UTF8, 0, in, -1, NULL, 0); + out = (TCHAR*)calloc (size, sizeof (TCHAR)); + MultiByteToWideChar(CP_UTF8, 0, in, -1, out, size); + } + return out; +} + +char* utf16to8(const TCHAR* in) { + char* out; + if (!in || _tcslen(in) == 0) { + out = (char*)calloc (1, sizeof(char)); + } else { + int len = WideCharToMultiByte(CP_UTF8, 0, in, -1, NULL, 0, 0, 0); + out = (char*)calloc (len, sizeof(char)); + WideCharToMultiByte(CP_UTF8, 0, in, -1, out, len, 0, 0); + } + return out; +} + +// https://stackoverflow.com/a/25023604/6121703 +int detectCodePage(const unsigned char *data) { + return strncmp(data, "\xEF\xBB\xBF", 3) == 0 ? CP_UTF8 : // BOM + strncmp(data, "\xFE\xFF", 2) == 0 ? CP_UTF16BE : // BOM + strncmp(data, "\xFF\xFE", 2) == 0 ? CP_UTF16LE : // BOM + strncmp(data, "\x00\x3C", 2) == 0 ? CP_UTF16BE : // < + strncmp(data, "\x3C\x00", 2) == 0 ? CP_UTF16LE : // < + isUtf8(data) ? CP_UTF8 : + CP_ACP; +} + +void setClipboardText(const TCHAR* text) { + int len = (_tcslen(text) + 1) * sizeof(TCHAR); + HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, len); + memcpy(GlobalLock(hMem), text, len); + GlobalUnlock(hMem); + OpenClipboard(0); + EmptyClipboard(); + SetClipboardData(CF_UNICODETEXT, hMem); + CloseClipboard(); +} + +BOOL isNumber(const TCHAR* val) { + int len = _tcslen(val); + BOOL res = TRUE; + int pCount = 0; + for (int i = 0; res && i < len; i++) { + pCount += val[i] == TEXT('.'); + res = _istdigit(val[i]) || val[i] == TEXT('.'); + } + return res && pCount < 2; +} + +BOOL isEmpty (const char* s) { + BOOL res = TRUE; + for (int i = 0; s && res && i < strlen(s); i++) + res = !isgraph(s[i]); + + return res; +} + +// https://stackoverflow.com/a/1031773/6121703 +BOOL isUtf8(const char * string) { + if (!string) + return FALSE; + + const unsigned char * bytes = (const unsigned char *)string; + while (*bytes) { + if((bytes[0] == 0x09 || bytes[0] == 0x0A || bytes[0] == 0x0D || (0x20 <= bytes[0] && bytes[0] <= 0x7E))) { + bytes += 1; + continue; + } + + if (((0xC2 <= bytes[0] && bytes[0] <= 0xDF) && (0x80 <= bytes[1] && bytes[1] <= 0xBF))) { + bytes += 2; + continue; + } + + if ((bytes[0] == 0xE0 && (0xA0 <= bytes[1] && bytes[1] <= 0xBF) && (0x80 <= bytes[2] && bytes[2] <= 0xBF)) || + (((0xE1 <= bytes[0] && bytes[0] <= 0xEC) || bytes[0] == 0xEE || bytes[0] == 0xEF) && (0x80 <= bytes[1] && bytes[1] <= 0xBF) && (0x80 <= bytes[2] && bytes[2] <= 0xBF)) || + (bytes[0] == 0xED && (0x80 <= bytes[1] && bytes[1] <= 0x9F) && (0x80 <= bytes[2] && bytes[2] <= 0xBF)) + ) { + bytes += 3; + continue; + } + + if ((bytes[0] == 0xF0 && (0x90 <= bytes[1] && bytes[1] <= 0xBF) && (0x80 <= bytes[2] && bytes[2] <= 0xBF) && (0x80 <= bytes[3] && bytes[3] <= 0xBF)) || + ((0xF1 <= bytes[0] && bytes[0] <= 0xF3) && (0x80 <= bytes[1] && bytes[1] <= 0xBF) && (0x80 <= bytes[2] && bytes[2] <= 0xBF) && (0x80 <= bytes[3] && bytes[3] <= 0xBF)) || + (bytes[0] == 0xF4 && (0x80 <= bytes[1] && bytes[1] <= 0x8F) && (0x80 <= bytes[2] && bytes[2] <= 0xBF) && (0x80 <= bytes[3] && bytes[3] <= 0xBF)) + ) { + bytes += 4; + continue; + } + + return FALSE; + } + + return TRUE; +} + +HTREEITEM TreeView_AddItem (HWND hTreeWnd, TCHAR* caption, HTREEITEM parent, LPARAM lParam) { + TVITEM tvi = {0}; + TVINSERTSTRUCT tvins = {0}; + tvi.mask = TVIF_TEXT | TVIF_PARAM; + tvi.pszText = caption; + tvi.cchTextMax = _tcslen(caption) + 1; + tvi.lParam = lParam; + + tvins.item = tvi; + tvins.hInsertAfter = TVI_LAST; + tvins.hParent = parent; + return (HTREEITEM)SendMessage(hTreeWnd, TVM_INSERTITEM, 0, (LPARAM)(LPTVINSERTSTRUCT)&tvins); +}; + +int TreeView_GetItemText(HWND hTreeWnd, HTREEITEM hItem, TCHAR* buf, int maxLen) { + TV_ITEM tv = {0}; + tv.mask = TVIF_TEXT; + tv.hItem = hItem; + tv.cchTextMax = maxLen; + tv.pszText = buf; + return TreeView_GetItem(hTreeWnd, &tv); +} + +LPARAM TreeView_GetItemParam(HWND hTreeWnd, HTREEITEM hItem) { + TV_ITEM tv = {0}; + tv.mask = TVIF_PARAM; + tv.hItem = hItem; + + return TreeView_GetItem(hTreeWnd, &tv) ? tv.lParam : 0; +} + +int TreeView_SetItemText(HWND hTreeWnd, HTREEITEM hItem, TCHAR* text) { + TV_ITEM tv = {0}; + tv.mask = TVIF_TEXT; + tv.hItem = hItem; + tv.cchTextMax = _tcslen(text) + 1; + tv.pszText = text; + return TreeView_SetItem(hTreeWnd, &tv); +} + +int ListView_AddColumn(HWND hListWnd, TCHAR* colName) { + int colNo = Header_GetItemCount(ListView_GetHeader(hListWnd)); + LVCOLUMN lvc = {0}; + lvc.mask = LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM; + lvc.iSubItem = colNo; + lvc.pszText = colName; + lvc.cchTextMax = _tcslen(colName) + 1; + lvc.cx = 100; + return ListView_InsertColumn(hListWnd, colNo, &lvc); +} + +int Header_GetItemText(HWND hWnd, int i, TCHAR* pszText, int cchTextMax) { + if (i < 0) + return FALSE; + + TCHAR* buf = calloc(cchTextMax + 1, sizeof(TCHAR)); + + HDITEM hdi = {0}; + hdi.mask = HDI_TEXT; + hdi.pszText = buf; + hdi.cchTextMax = cchTextMax; + int rc = Header_GetItem(hWnd, i, &hdi); + + _tcsncpy(pszText, buf, cchTextMax); + free(buf); + return rc; +} \ No newline at end of file diff --git a/xml.c b/xml.c new file mode 100644 index 0000000..2e81753 --- /dev/null +++ b/xml.c @@ -0,0 +1,818 @@ +#include +#include + +#include "xml.h" + +#define WHITESPACE " \t\r\n" + +#define TAG_ELEMENT_OPEN 1 +#define TAG_ELEMENT_CLOSE 2 +#define TAG_PI 3 +#define TAG_TYPE 4 +#define TAG_COMMENT 5 +#define TAG_CDATA 6 + +typedef struct xml_state { + /* the root element */ + struct xml_element *root; + + /* internal state variables */ + struct xml_element *current; + struct xml_tag_pattern *tag; + size_t length; + size_t cursor; + int empty; + const char* data; + const char *(*parser)(struct xml_state *, const char *); +} xml_state; + +struct xml_tag_pattern { + int type; + const char *open; + size_t open_len; + const char *close; +} xml_tag_patterns[] = { + {TAG_ELEMENT_OPEN, "<", 1, ">"}, + {TAG_ELEMENT_CLOSE, ""}, + {TAG_PI, ""}, + {TAG_TYPE, ""}, + {TAG_COMMENT, ""}, + {TAG_CDATA, ""}, + {0, 0, 0, 0} +}; + +/***************************************************************************** + * STRING OPERATIONS + ****************************************************************************/ + +/** + * Return length of quoted string respecting escaped characters + * + * @param s - first character after quote + * @param q - quote character + */ +static size_t xml_quotedspn(const char *s, char q) { + const char *first = s; + + while (*++s) { + if (*s == '\\') { + ++s; + continue; + } + + if (*s == q) { + return s - first; + } + } + + return 0; +} + +#ifdef WIN32 +/** + * Copies n bytes of given string + * + * @param s - string to copy + * @param n - number of bytes + */ +static char *strndup(const char *s, size_t n) { + char *r; + + if (!(r = calloc(n + 1, sizeof(char))) || + !memcpy(r, s, n)) { + return NULL; + } + + return r; +} +#endif + +/** + * Append string + * + * @param dest - address of string to append to + * @param dest_len - address of length of string + * @param src - string to append + * @param src_len - length of string to append + */ +static char *xml_string_append( + char **dest, + size_t *dest_len, + const char *src, + size_t src_len) { + char *n; + + if (src_len < 1) { + return *dest; + } + + if (!*dest) { + if ((*dest = strndup(src, src_len))) { + *dest_len += src_len; + } + } else if ((n = realloc(*dest, *dest_len + src_len + 1))) { + *dest = n; + n += *dest_len; + + *n = 0; + strncat(n, src, src_len); + + *dest_len += src_len; + } + + return *dest; +} + +/***************************************************************************** + * CREATING AND MODIFYING ELEMENTS + ****************************************************************************/ + +/** + * Add child to parent element + * + * @param p - parent element + * @param c - child element + */ +static void xml_element_add( + struct xml_element *p, + struct xml_element *c) { + c->parent = p; + + if (p->first_child) { + p->last_child->next = c; + p->last_child = c; + } else { + p->first_child = p->last_child = c; + } + + ++p->child_count; +} + +/** + * Create a new element + * + * @param parent - parent element + */ +static struct xml_element *xml_element_create(struct xml_element *parent, int offset) { + struct xml_element *e; + + if (!(e = calloc(1, sizeof(struct xml_element)))) { + return NULL; + } + e->offset = offset; + + if (parent) { + xml_element_add(parent, e); + } + + return e; +} + +/***************************************************************************** + * CREATING AND MODIFYING ATTRIBUTES + ****************************************************************************/ + +/** + * Add attribute to element + * + * @param e - element + * @param a - attribute + */ +static void xml_attribute_add( + struct xml_element *e, + struct xml_attribute *a) { + if (e->first_attribute) { + e->last_attribute->next = a; + e->last_attribute = a; + } else { + e->first_attribute = e->last_attribute = a; + } + + ++e->attribute_count; +} + +/** + * Create a new attribute + * + * @param parent - parent element + */ +static struct xml_attribute *xml_attribute_create( + struct xml_element *parent) { + struct xml_attribute *a; + + if (!(a = calloc(1, sizeof(struct xml_attribute)))) { + return NULL; + } + + if (parent) { + xml_attribute_add(parent, a); + } + + return a; +} + +/***************************************************************************** + * APPENDING KEY/VALUE + ****************************************************************************/ + +/** + * Append character data to current element + * + * @param st - state + * @param d - begin of data + * @param l - length of data + */ +static int xml_value_append(struct xml_state *st, const char *d, size_t l) { + int pos = d - st->data; + if (!st->length && + !(st->current = xml_element_create(st->current, pos))) { + return -1; + } + + if (!xml_string_append( + &st->current->value, + &st->length, + d, + l)) { + return -1; + } + + return 0; +} + +/** + * Append tag data to current element + * + * @param st - state + * @param d - begin of data + * @param l - length of data + */ +static int xml_key_append(struct xml_state *st, const char *d, size_t l) { + if (!st->current) { + return -1; + } + + /* ignore data for closing elements */ + if (st->tag->type == TAG_ELEMENT_CLOSE) { + return 0; + } + + /* append start of pattern for special tag types */ + if (!st->length && + st->tag->open_len > 1 && + !xml_string_append( + &st->current->key, + &st->length, + st->tag->open + 1, + st->tag->open_len - 1)) { + return -1; + } + + if (!xml_string_append( + &st->current->key, + &st->length, + d, + l)) { + return -1; + } + + return 0; +} + +/***************************************************************************** + * PARSING ATTRIBUTES + ****************************************************************************/ + +/** + * Parse attributes + * + * @param e - element + * @param from - first character after tag name + */ +static int xml_parse_attributes(struct xml_element *e, char *from) { + while (*from) { + struct xml_attribute *a; + size_t p; + char *key = NULL; + char *value = NULL; + size_t key_len = 0; + size_t value_len = 0; + + /* skip leading white space */ + from += strspn(from, WHITESPACE); + + /* search for first character that is not part of a name */ + p = strcspn(from, "="WHITESPACE); + + if (p < 1) { + break; + } + + key = from; + key_len = p; + + /* move after argument name */ + from += p; + + /* skip white space before next control character */ + from += strspn(from, WHITESPACE); + + if (*from == '=') { + char q = 0; + + /* move after '=' and skip leading white space */ + ++from; + from += strspn(from, WHITESPACE); + + if (!*from) { + break; + } else if (*from == '\'') { + p = xml_quotedspn(++from, (q = '\'')); + } else if (*from == '"') { + p = xml_quotedspn(++from, (q = '"')); + } else { + /* argument data is unqouted */ + p = strcspn(from, WHITESPACE); + } + + value = from; + value_len = p; + from += p; + + if (*from == q) { + ++from; + } + } + + if (!(a = xml_attribute_create(e))) { + return -1; + } + + if (key) { + *(key + key_len) = 0; + a->key = key; + } + + if (value) { + *(value + value_len) = 0; + a->value = value; + } + } + + return 0; +} + +/***************************************************************************** + * PARSING ELEMENTS + ****************************************************************************/ + +/* forward declarations */ +static const char *xml_parse_content(struct xml_state *, const char *); + +/** + * Close element + * + * @param st - state + */ +static void xml_close_element(struct xml_state *st, const char* d) { + st->current->length = d - st->data - st->current->offset; + st->current = st->current->parent; +} + +/** + * Close tag + * + * @param st - state + */ +static void xml_close_tag(struct xml_state *st) { + st->tag = NULL; + st->length = 0; + st->cursor = 0; + st->empty = 0; + st->parser = xml_parse_content; +} + +/** + * Parse end of tag name for empty element marker + * + * @param st - state + */ +static void xml_check_empty(struct xml_state *st) { + char *p = st->current->key + strlen(st->current->key) - 1; + + for (; p >= st->current->key; --p) { + /* ignore trailing white space; this isn't required + * by the spec but probably better to have */ + if (strchr(WHITESPACE, *p)) { + continue; + } + + if (*p == '/') { + *p = 0; + st->empty = 1; + } + + break; + } +} + +/** + * Parse tag name + * + * @param st - state + */ +static int xml_parse_tag_name(struct xml_state *st) { + char *p = st->current->key; + + xml_check_empty(st); + p += strcspn(p, WHITESPACE); + + if (!*p) { + /* key ends with the name */ + return 0; + } + + /* terminate name and skip further white space */ + *p++ = 0; + p += strspn(p, WHITESPACE); + + if (*p && xml_parse_attributes(st->current, p)) { + return -1; + } + + return 0; +} + +/** + * Parse tag until terminating pattern + * + * @param st - state + * @param d - XML data + */ +static const char *xml_parse_tag_body(struct xml_state *st, const char *d) { + while (*d) { + const char *m = NULL; + + if (!st->cursor) { + /* find first character of terminating pattern */ + m = strchr(d, st->tag->close[0]); + } else { + /* find next character of terminating pattern */ + for (;;) { + if (st->tag->close[st->cursor] == *d) { + m = d; + } + + /* break if next character does match or cursor + * is at the first character */ + if (m || !st->cursor) { + break; + } + + /* append leading part of pattern since it + * is data if d doesn't match */ + if (xml_key_append(st, st->tag->close, st->cursor)) { + return NULL; + } + + /* if cursor has already moved into a pattern + * but d doesn't match, d may still match the + * beginning of a pattern */ + st->cursor = 0; + } + + /* start over if no match was found and cursor + * is at first character of the pattern */ + if (!m && !st->cursor) { + continue; + } + } + + if (m) { + /* put data until this match into tag name */ + if (!st->cursor && xml_key_append(st, d, m - d)) { + return NULL; + } + + d = ++m; + ++st->cursor; + + /* terminating pattern complete */ + if (!st->tag->close[st->cursor]) { + /* append termination pattern for special tag types */ + if (st->cursor > 1 && + !xml_string_append( + &st->current->key, + &st->length, + st->tag->close, + st->cursor - 1)) { + return NULL; + } + + if (st->tag->type == TAG_ELEMENT_OPEN && + xml_parse_tag_name(st)) { + return NULL; + } + + if (st->tag->type != TAG_ELEMENT_OPEN || + st->empty) { + xml_close_element(st, d); + } + + xml_close_tag(st); + break; + } + } else { + /* append all the rest */ + size_t l = strlen(d); + + if (xml_key_append(st, d, l)) { + return NULL; + } + + /* move to end of data */ + d += l; + + break; + } + } + + return d; +} + +/** + * Parse beginning of a tag to determine its type (and terminating pattern) + * + * @param st - state + * @param d - XML data + */ +static const char *xml_parse_tag_opening(struct xml_state *st, + const char *d) { + for (; *d; ++d) { + struct xml_tag_pattern *p = xml_tag_patterns; + + /* check character against all opening patterns */ + for (;;) { + for (; p->type; ++p) { + if (p->open_len > st->cursor && + p->open[st->cursor] == *d) { + break; + } + } + + /* break if pattern is found or cursor is at + * first character */ + if (p->type || !st->cursor) { + break; + } + + /* if cursor has already moved into a pattern but + * d doesn't match, d may still match the beginning + * of a pattern */ + st->cursor = 0; + } + + /* if character doesn't match any pattern anymore + * take the latest matching pattern */ + if (!p->type) { + if (!st->tag) { + return NULL; + } + + if (st->length > 0) { + xml_close_element(st, d); + } + + st->length = 0; + st->cursor = 0; + st->parser = xml_parse_tag_body; + + /* create child element */ + int offset = d - st->data - st->tag->open_len; + if (st->tag->type != TAG_ELEMENT_CLOSE && + !(st->current = xml_element_create(st->current, offset))) { + return NULL; + } + + break; + } + + ++st->cursor; + st->tag = p; + } + + return d; +} + +/** + * Parse character data of active element + * + * @param st - state + * @param d - XML data + */ +static const char *xml_parse_content(struct xml_state *st, const char *d) { + const char *end = d + strcspn(d, "<"); + + if (end > d && xml_value_append(st, d, end - d)) { + return NULL; + } + + if (*end == '<') { + st->parser = xml_parse_tag_opening; + } + + return end; +} + +/** + * Parse (next) chunk of a XML document + * + * @param st - parsing status + * @param d - XML chunk + */ +int xml_parse_chunk(struct xml_state *st, const char *d) { + if (!d) { + return -1; + } + + if (!st->root) { + st->current = st->root = xml_element_create(NULL, 0); + } + + if (!st->parser) { + xml_close_tag(st); + } + + while (*d) { + if (!(d = st->parser(st, d))) { + return -1; + } + } + + return 0; +} + +/** + * Parse XML document + * + * @param data - XML string + */ +struct xml_element *xml_parse(const char *data) { + struct xml_state st; + + memset(&st, 0, sizeof(st)); + st.data = data; + + if (!xml_parse_chunk(&st, data) && st.root) { + return st.root; + } + + return NULL; +} + +/***************************************************************************** + * FREE MEMORY + ****************************************************************************/ + +/** + * Free XML element tree + * + * @param e - root element + */ +void xml_free(struct xml_element *e) { + struct xml_element *c, *n; + + if (!e) { + return; + } + + for (c = e->first_child; c; c = n) { + n = c->next; + xml_free(c); + } + + /* free attributes */ + { + struct xml_attribute *a, *na; + + for (a = e->first_attribute; a; a = na) { + na = a->next; + + /* don't free key/value because they're + * just pointers into e->key */ + free(a); + } + } + + free(e->key); + free(e->value); + free(e); +} + +/***************************************************************************** + * CHILD ELEMENT LOCATION + ****************************************************************************/ + +/** + * Returns child element by key + * + * @param e - first attribute + * @param key - attribute key + */ +struct xml_element *xml_find_element( + struct xml_element *e, + const char *key) { + xml_element* s = e ? e->first_child : NULL; + for (; s; s = s->next) { + if (!strcasecmp(s->key, key)) { + return s; + } + } + + return NULL; +} + +/***************************************************************************** + * ATTRIBUTE LOCATION + ****************************************************************************/ + +/** + * Returns attribute by key + * + * @param a - first attribute + * @param key - attribute key + */ +struct xml_attribute *xml_find_attribute( + struct xml_element *e, + const char *key) { + xml_attribute* a = e ? e->first_attribute : NULL; + for (; a; a = a->next) { + if (!strcasecmp(a->key, key)) { + return a; + } + } + + return NULL; +} + +/***************************************************************************** + * CONTENT CONCATENATION + ****************************************************************************/ + +/** + * Calculate size of all child values + * + * @param e - element + */ +static size_t xml_content_len(struct xml_element *e) { + size_t s = 0; + + for (e = e->first_child; e; e = e->next) { + if (e->value) { + s += strlen(e->value); + } else { + s += xml_content_len(e); + } + } + + return s; +} + +/** + * Copy elements into pre-calculated buffer + * + * @param e - element + * @param t - target + */ +static void xml_content_cpy(struct xml_element *e, char **t) { + for (e = e->first_child; e; e = e->next) { + if (e->value) { + strcpy(*t, e->value); + *t += strlen(e->value); + } else { + xml_content_cpy(e, t); + } + } +} + +/** + * Return concatenated content of element and all of its children + * + * @param e - element + */ +char *xml_content(struct xml_element *e) { + size_t l; + char *s; + char *t; + + if (!e || + (l = xml_content_len(e)) < 1 || + !(s = malloc(++l))) { + return NULL; + } + + t = s; + xml_content_cpy(e, &t); + + return s; +} diff --git a/xml.h b/xml.h new file mode 100644 index 0000000..36dbf45 --- /dev/null +++ b/xml.h @@ -0,0 +1,59 @@ +// Based on https://github.com/markusfisch/libxml +#ifndef _xml_h_ +#define _xml_h_ + +typedef struct xml_attribute { + /* Argument name */ + char *key; + + /* Argument value */ + char *value; + + /* Pointer to next argument. May be NULL. */ + struct xml_attribute *next; +} xml_attribute; + +typedef struct xml_element { + /* The tag name if this is a tag element or NULL if this + * element represents character data. */ + char *key; + + /* Character data segment of parent XML element or NULL if + * this is a tag element. "key" and "value" are exclusive + * because character data is treated as "nameless" child + * element. So each xml_element can have either a "key" + * or a "value" but not both. */ + char *value; + + /* Parent element. May be NULL. */ + struct xml_element *parent; + + /* First child element. May be NULL. */ + struct xml_element *first_child; + + /* Last child element. May be NULL. */ + struct xml_element *last_child; + + /* Pointer to next sibling. May be NULL. */ + struct xml_element *next; + + /* Added: child count, XML offset and XML length */ + int child_count; + int offset; + int length; + + /* First and last attribute. Both may be NULL. */ + xml_attribute *first_attribute, *last_attribute; + + int attribute_count; +} xml_element; + +struct xml_element *xml_parse(const char *); +void xml_free(struct xml_element *); + +struct xml_element *xml_find_element(struct xml_element*, const char*); +struct xml_attribute *xml_find_attribute(struct xml_element*, const char*); + +char *xml_content(struct xml_element *); + +#endif diff --git a/xmltab.png b/xmltab.png new file mode 100644 index 0000000000000000000000000000000000000000..c4a922d22ceec5f603101e9966ec706f828738f4 GIT binary patch literal 29878 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 OBcna0yYuxNBmM=VeF}R3 literal 0 HcmV?d00001