diff --git a/.gitignore b/.gitignore index ec2fe9b..347653b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,97 @@ -# System Files and Directories -.idea/ -.DS_Store +# ignore everything +/* -# Build Directories -*cmake-build*/ +# include generic files +!/.gitignore +!/LICENSE +!/NOTICE +!/README.md -# Visual Studio -.vs/ -out/ +# include root directory files +!/cmake-cleanup.sh +!/CMakeLists.txt +!/Makefile.GNU +!/make-gnu.sh +!/vcpkg.json -# other -*.so -*.dylib -*.a -*.xlsx \ No newline at end of file +# include Benchmarks folder +!/Benchmarks +# exclude everything *in* Benchmarks folder +/Benchmarks/* +# re-include specifically what is desired from Benchmarks folder +!/Benchmarks/*.cpp +!/Benchmarks/*.txt + +# include Documentation folder +!/Documentation +# exclude everything *in* Documentation folder +/Documentation/* +# re-include specifically what is desired from Documentation folder +!/Documentation/*.txt +!/Documentation/*.css +!/Documentation/*.in +!/Documentation/*.xml + +# include Examples folder +!/Examples +# exclude everything *in* Examples folder +/Examples/* +# re-include specifically what is desired from Examples folder +!/Examples/cmake +!/Examples/cmake/* +!/Examples/CMakeLists.txt +!/Examples/*.cpp +!/Examples/external +!/Examples/external/* + +# include gnu-make-crutch folder +!/gnu-make-crutch +# exclude everything *in* gnu-make-crutch folder +/gnu-make-crutch/* +# re-include specifically what is desired from gnu-make-crutch folder +!/gnu-make-crutch/OpenXLSX-Exports.hpp + +# include OpenXLSX folder +!/OpenXLSX +# exclude everything *in* OpenXLSX folder +/OpenXLSX/* +# re-include specifically what is desired from OpenXLSX folder +!/OpenXLSX/CMakeLists.txt +!/OpenXLSX/external +!/OpenXLSX/external/* +!/OpenXLSX/headers +!/OpenXLSX/headers/* +!/OpenXLSX/OpenXLSXConfig.cmake +!/OpenXLSX/OpenXLSX.hpp +!/OpenXLSX/sources +!/OpenXLSX/sources/* + +# include Tests folder +!/Tests +# exclude everything *in* Tests folder +/Tests/* +# re-include specifically what is desired from Tests folder +!/Tests/catch +!/Tests/catch/*.hpp +!/Tests/CMakeLists.txt +!/Tests/*.cpp + + +# ## OLD gitignore rules below: +# +# # System Files and Directories +# .idea/ +# .DS_Store +# +# # Build Directories +# *cmake-build*/ +# +# # Visual Studio +# .vs/ +# out/ +# +# # other +# *.so +# *.dylib +# *.a +# *.xlsx diff --git a/CMakeLists.txt b/CMakeLists.txt old mode 100755 new mode 100644 diff --git a/Makefile.GNU b/Makefile.GNU new file mode 100644 index 0000000..f489498 --- /dev/null +++ b/Makefile.GNU @@ -0,0 +1,380 @@ +# GNU Makefile for OpenXLSX modules & demos +# Version: 2024-07-08 20:45 CEST + +# NOTE: lib and library are available as aliases for OpenXLSX, to build the static library file +TARGETS=OpenXLSX Demo1 Demo2 Demo3 Demo4 Demo5 Demo6 Demo7 Demo8 + +DEMOS_SRC_DIR=Examples +BIN_DIR=output + +CC=gcc +CXX=g++ + +# /usr/bin/ar: create archive without symbol table +AR=ar +# /usr/bin/ranlib: add symbol table to archive created with ar +RANLIB=ranlib + + +OPTIMIZATION_FLAGS= +#OPTIMIZATION_FLAGS=-O3 + +# === BEGIN: Detect OS platform based on gcc compiler defines === +# Actual detection command: +DETECT_CMD="echo | $(CC) -dM -E -" + +# Test commands to "simulate" a platform +# DETECT_CMD="echo \"\#define WIN32 1\"" +# DETECT_CMD="echo \"\#define _WIN32 1\"" +# DETECT_CMD="echo \"\#define __WIN32__ 1\"" +# DETECT_CMD="echo \"\#define _WIN64 1\"" +# DETECT_CMD="printf '\#define WIN32 1\n\#define _WIN32 1\n\#define __WIN32__ 1\n\#define _WIN64 1'" # test all defines together with line breaks +# DETECT_CMD="echo \"\#define __linux__ 1\"" +# DETECT_CMD="echo \"\#define __CYGWIN__ 1\"" + +__linux__ := $(shell "$(DETECT_CMD)" | grep "\b__linux__\b") +ifdef __linux__ + Linux=yes +else + Linux=no +endif + +WIN32 := $(shell "$(DETECT_CMD)" | grep "\bWIN32\b") +_WIN32 := $(shell "$(DETECT_CMD)" | grep "\b_WIN32\b") +__WIN32__ := $(shell "$(DETECT_CMD)" | grep "\b__WIN32__\b") +_WIN64 := $(shell "$(DETECT_CMD)" | grep "\b_WIN64\b") +ifdef WIN32 + Windows=yes +else ifdef _WIN32 + Windows=yes +else ifdef __WIN32__ + Windows=yes +else ifdef _WIN64 + Windows=yes +else + Windows=no +endif + +__CYGWIN__ := $(shell "$(DETECT_CMD)" | grep "\b__CYGWIN__\b") +ifdef __CYGWIN__ + Cygwin=yes +else + Cygwin=no +endif +# === END: detect OS platform based on gcc compiler defines === + +OPENXLSX_DIR=OpenXLSX +SRC_DIR=sources +INCLUDE_DIR=headers +OBJ_DIR=obj +STATIC_LIBRARY=libOpenXLSX.a + +USE_NOWIDE=no +ifeq ("$(Windows)", "yes") + # enable nowide on windows. CAUTION: do not append this comment to the variable assignment, it will break the ifeq check below + USE_NOWIDE=yes +endif + +# SHARED_SUBDIR=shared +EXTERNAL_SUBDIR=external +PUGIXML_SUBDIR=$(EXTERNAL_SUBDIR)/pugixml +ZIPPY_SUBDIR=$(EXTERNAL_SUBDIR)/zippy + +# library / utility objects +# OBJS_LICENSE=license.o +# OBJS_SHARED=$(OBJS_LICENSE) +OBJS_PUGIXML= # used as header-only module +OBJS_ZIPPY= # header-only module +OBJS_OPENXLSX=XLCell.o XLCellIterator.o XLCellRange.o XLCellReference.o XLCellValue.o XLColor.o XLColumn.o XLContentTypes.o XLDateTime.o XLDocument.o XLFormula.o XLProperties.o XLRelationships.o XLRow.o XLRowData.o XLSharedStrings.o XLSheet.o XLWorkbook.o XLXmlData.o XLXmlFile.o XLXmlParser.o XLZipArchive.o + +# create a version of OBJS_OPENXLSX that already has the correct prefix so that it can be used for linking without further modification +OBJS_OPENXLSX_PREFIXED=$(addprefix $(OBJ_DIR)/$(OPENXLSX_DIR)/,$(OBJS_OPENXLSX)) + +# OBJS_DEMOS=$(OBJS_OPENXLSX_PREFIXED) # no longer needed - using static library instead +OBJS_DEMOS=$(BIN_DIR)/$(STATIC_LIBRARY) # use static library for linking demos + +PROJECT_FLAGS= +ifeq ("$(USE_NOWIDE)", "yes") + PROJECT_FLAGS=-DENABLE_NOWIDE +endif + + +# additional include directories for 1) OPENXLSX project headers and 2) external nowide (parent) folder +ADDITIONAL_INCLUDE_FLAGS=-I$(OPENXLSX_DIR) -I$(OPENXLSX_DIR)/$(EXTERNAL_SUBDIR) + +# application objects +OBJS_DEMO1=Demo1.o +OBJS_OPENXLSX_DEMO1=$(OBJS_DEMOS) +LDLIBS_DEMO1= + +OBJS_DEMO2=Demo2.o +OBJS_OPENXLSX_DEMO2=$(OBJS_DEMOS) +LDLIBS_DEMO2= + +OBJS_DEMO3=Demo3.o +OBJS_OPENXLSX_DEMO3=$(OBJS_DEMOS) +LDLIBS_DEMO3= + +OBJS_DEMO4=Demo4.o +OBJS_OPENXLSX_DEMO4=$(OBJS_DEMOS) +LDLIBS_DEMO4= + +OBJS_DEMO5=Demo5.o +OBJS_OPENXLSX_DEMO5=$(OBJS_DEMOS) +LDLIBS_DEMO5= + +OBJS_DEMO6=Demo6.o +OBJS_OPENXLSX_DEMO6=$(OBJS_DEMOS) +LDLIBS_DEMO6= + +OBJS_DEMO7=Demo7.o +OBJS_OPENXLSX_DEMO7=$(OBJS_DEMOS) +LDLIBS_DEMO7= + +OBJS_DEMO8=Demo8.o +OBJS_OPENXLSX_DEMO8=$(OBJS_DEMOS) +LDLIBS_DEMO8= + +SANITIZE_FLAGS= +# SANITIZE_FLAGS=-fsanitize=address +# SANITIZE_FLAGS=-fsanitize=address -fsanitize=leak +SANITIZE_LIBS= +# SANITIZE_LIBS=-llsan + +# 2024-07-08 warning state: +# misleading-indentation: 0 warnings +# unused-function: 1 warning: OpenXLSX/sources/XLProperties.cpp:69: std::vector headingPairsCategoriesStrings(XMLNode docNode) +# sign-compare: 7 warnings +# unknown-pragmas: 1586(!) +DISABLED_WARNING_FLAGS=-Wno-unknown-pragmas -Wno-sign-compare + +GENERIC_FLAGS=$(ADDITIONAL_INCLUDE_FLAGS) $(PROJECT_FLAGS) -fno-common $(SANITIZE_FLAGS) $(OPTIMIZATION_FLAGS) -Wall -Wformat -Wformat-signedness $(DISABLED_WARNING_FLAGS) +# WARNING: there be dragons here: +# GENERIC_FLAGS=-I$(INCLUDE_DIR) -fno-common $(OPTIMIZATION_FLAGS) -Wall -Wformat -Wformat-signedness -Wpedantic -Wextra -Werror + +CPPFLAGS=$(GENERIC_FLAGS) -std=c++17 +# CPPFLAGS=$(GENERIC_FLAGS) -std=c++17 -D_GNU_SOURCE -D_POSIX_C_SOURCE=200809L -D_XOPEN_SOURCE=700 + +LDFLAGS=$(SANITIZE_FLAGS) +# LDFLAGS=-fno-common + +# TEST: truncate binary to actually used code (https://gcc.gnu.org/onlinedocs/gnat_ugn/Compilation-options.html) +# GENERIC_FLAGS+= -ffunction-sections -fdata-sections +# LDFLAGS+= -Wl,--gc-sections + + +# precompiled libs go here +LDLIBS=$(SANITIZE_LIBS) +# LDLIBS=-lrt -pthread -lboost_program_options $(SANITIZE_LIBS) # example to add libraries if needed + + +## additional tools for text substitution: +# call tools like so: +# VAR = MixedCaseText +# LOWER_VAR = $(call lc,$(VAR)) +# create all lowercase variable: +# lc = $(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst F,f,$(subst G,g,$(subst H,h,$(subst I,i,$(subst J,j,$(subst K,k,$(subst L,l,$(subst M,m,$(subst N,n,$(subst O,o,$(subst P,p,$(subst Q,q,$(subst R,r,$(subst S,s,$(subst T,t,$(subst U,u,$(subst V,v,$(subst W,w,$(subst X,x,$(subst Y,y,$(subst Z,z,$1)))))))))))))))))))))))))) +# create all uppercase variable: +uc = $(subst a,A,$(subst b,B,$(subst c,C,$(subst d,D,$(subst e,E,$(subst f,F,$(subst g,G,$(subst h,H,$(subst i,I,$(subst j,J,$(subst k,K,$(subst l,L,$(subst m,M,$(subst n,N,$(subst o,O,$(subst p,P,$(subst q,Q,$(subst r,R,$(subst s,S,$(subst t,T,$(subst u,U,$(subst v,V,$(subst w,W,$(subst x,X,$(subst y,Y,$(subst z,Z,$1)))))))))))))))))))))))))) + + +## gnu make override to define the default make directive, only takes a single target +.DEFAULT_GOAL := default +default: $(TARGETS) + +all: $(TARGETS) + +# the $(NOOP) rules are only needed for the dependency / forward to $(BIN_DIR) +Demo1: $(BIN_DIR)/Demo1 + $(NOOP) +Demo2: $(BIN_DIR)/Demo2 + $(NOOP) +Demo3: $(BIN_DIR)/Demo3 + $(NOOP) +Demo4: $(BIN_DIR)/Demo4 + $(NOOP) +Demo5: $(BIN_DIR)/Demo5 + $(NOOP) +Demo6: $(BIN_DIR)/Demo6 + $(NOOP) +Demo7: $(BIN_DIR)/Demo7 + $(NOOP) +Demo8: $(BIN_DIR)/Demo8 + $(NOOP) + +# re-direct from aliases +lib: OpenXLSX +library: OpenXLSX + +# static library target +OpenXLSX: $(BIN_DIR)/$(STATIC_LIBRARY) + +# delete existing file, create static library archive file & add symbol table to archive +# NOTE: if existing library file is not explicitly deleted, ar will fail to update a modified object file +$(BIN_DIR)/$(STATIC_LIBRARY): $(OBJS_OPENXLSX_PREFIXED) | $(BIN_DIR) + rm -f $@ # explicitly delete existing library file + $(AR) qc $@ $(addprefix $(OBJ_DIR)/$(OPENXLSX_DIR)/,$(OBJS_OPENXLSX)) + $(RANLIB) $@ + +# complex rule for BIN_DIR targets: include objects from OBJS_ and libraries from LDLIBS_ +.SECONDEXPANSION: +$(BIN_DIR)/%: $$(addprefix $(OBJ_DIR)/,$$(OBJS_$$(call uc,$$*))) $$(OBJS_OPENXLSX_$$(call uc,$$*)) | $(BIN_DIR) + $(CXX) $(LDFLAGS) $^ $(LDLIBS) $(LDLIBS_$(call uc,$*)) -o $@ + +# rule for .cpp files in DEMOS_SRC_DIR +$(OBJ_DIR)/%.o: $(addprefix $(DEMOS_SRC_DIR)/,%.cpp) | $(OBJ_DIR) + $(CXX) $(CPPFLAGS) -I$(OPENXLSX_DIR)/$(INCLUDE_DIR) -c $< -o $@ + +# rule for OpenXLSX .cpp files +$(OBJ_DIR)/$(OPENXLSX_DIR)/%.o: $(OPENXLSX_DIR)/$(SRC_DIR)/%.cpp | $(OBJ_DIR) $(OBJ_DIR)/$(OPENXLSX_DIR)/ $(OPENXLSX_DIR)/OpenXLSX-Exports.hpp + $(CXX) $(CPPFLAGS) -I$(OPENXLSX_DIR)/$(INCLUDE_DIR) -I$(OPENXLSX_DIR)/$(PUGIXML_SUBDIR) -I$(OPENXLSX_DIR)/$(ZIPPY_SUBDIR) -c $< -o $@ + +# rule for pugixml .cpp files: N/A, as pugixml is used as header only +$(OBJ_DIR)/$(PUGIXML_SUBDIR)/%.o: $(OPENXLSX_DIR)/$(PUGIXML_SUBDIR)/%.cpp | $(OBJ_DIR) $(OBJ_DIR)/$(PUGIXML_SUBDIR)/ + $(CXX) $(CPPFLAGS) -I$(OPENXLSX_DIR)/$(PUGIXML_SUBDIR) -c $< -o $@ + +# rule for zippy .cpp files: N/A, as zippy is header only +$(OBJ_DIR)/$(ZIPPY_SUBDIR)/%.o: $(OPENXLSX_DIR)/$(ZIPPY_SUBDIR)/%.cpp | $(OBJ_DIR) $(OBJ_DIR)/$(ZIPPY_SUBDIR)/ + $(CXX) $(CPPFLAGS) -I$(OPENXLSX_DIR)/$(ZIPPY_SUBDIR) -c $< -o $@ + +$(OPENXLSX_DIR)/OpenXLSX-Exports.hpp: + cp gnu-make-crutch/OpenXLSX-Exports.hpp $@ + +# # rule for shared .cpp files +# $(OBJ_DIR)/$(SHARED_SUBDIR)/%.o: $(OPENXLSX_DIR)/$(SRC_DIR)/$(SHARED_SUBDIR)/%.cpp | $(OBJ_DIR) $(OBJ_DIR)/$(SHARED_SUBDIR)/ +# $(CXX) $(CPPFLAGS) -I$(INCLUDE_DIR)/$(SHARED_SUBDIR) -c $< -o $@ + + +# create BIN_DIR if not existing +$(BIN_DIR): + mkdir $@ + +# create OBJ_DIR if not existing +$(OBJ_DIR): + mkdir $@ + +# $(OBJ_DIR)/$(SHARED_SUBDIR)/: +# mkdir $@ + +$(OBJ_DIR)/$(OPENXLSX_DIR)/: + mkdir $@ + +$(OBJ_DIR)/$(PUGIXML_SUBDIR)/: + mkdir $@ + +$(OBJ_DIR)/$(ZIPPY_SUBDIR)/: + mkdir $@ + +# .SECONDARY with no prerequisites causes all targets to be treated +# as secondary (i.e., no target is removed because it is considered +# intermediate). +.SECONDARY: + +# indicate all rules without target output +.PHONY: default all clean cleanObjects cleanTargets echo lib library OpenXLSX + +clean: cleanObjects cleanTargets + +cleanObjects: + rm -f $(addprefix $(OBJ_DIR)/,$(OBJS_DEMO1) $(OBJS_DEMO2) $(OBJS_DEMO3) $(OBJS_DEMO4) $(OBJS_DEMO5) $(OBJS_DEMO6) $(OBJS_DEMO7) $(OBJS_DEMO8)) \ + $(BIN_DIR)/$(STATIC_LIBRARY) \ + $(OBJS_OPENXLSX_PREFIXED) \ + $(addprefix $(OBJ_DIR)/$(PUGIXML_SUBDIR)/,$(OBJS_PUGIXML)) \ + $(addprefix $(OBJ_DIR)/$(ZIPPY_SUBDIR)/,$(OBJS_ZIPPY)) +# $(addprefix $(OBJ_DIR)/$(SHARED_SUBDIR)/,$(OBJS_SHARED)) + +cleanTargets: + rm -f $(addprefix $(BIN_DIR)/,$(TARGETS)) + + +# have a look at the established configuration +echo: + @echo "OpenXLSX Makefile" + @echo "----------------------------------------------" + @echo "TARGETS: $(TARGETS)" + @echo "DEMOS_SRC_DIR: $(DEMOS_SRC_DIR)" + @echo "BIN_DIR: $(BIN_DIR)" + @echo + @echo "CC : $(CC)" + @echo "CXX : $(CXX)" + @echo "AR : $(AR)" + @echo "RANLIB: $(RANLIB)" + @echo + @echo "OPTIMIZATION_FLAGS: $(OPTIMIZATION_FLAGS)" + @echo + @echo "Platform detection variables:" + @echo "-----------------------------" + @echo 'DETECT_CMD: $(DETECT_CMD)' + @echo + @echo "__linux__ : $(__linux__)" + @echo "WIN32 : $(WIN32)" + @echo "_WIN32 : $(_WIN32)" + @echo "__WIN32__ : $(__WIN32__)" + @echo "_WIN64 : $(_WIN64)" + @echo "__CYGWIN__: $(__CYGWIN__)" + @echo + @echo "Platform detection results:" + @echo "---------------------------" + @echo "Linux : $(Linux)" + @echo "Windows: $(Windows)" + @echo "Cygwin : $(Cygwin)" + @echo + @echo "OpenXLSX project configuration:" + @echo "-------------------------------" + @echo "OPENXLSX_DIR: $(OPENXLSX_DIR)" + @echo "SRC_DIR: $(SRC_DIR)" + @echo "INCLUDE_DIR: $(INCLUDE_DIR)" + @echo "OBJ_DIR: $(OBJ_DIR)" + @echo "STATIC_LIBRARY: $(STATIC_LIBRARY)" + @echo + @echo "USE_NOWIDE: $(USE_NOWIDE)" + @echo +# @echo "SHARED_SUBDIR: $(SHARED_SUBDIR)" + @echo "EXTERNAL_SUBDIR: $(EXTERNAL_SUBDIR)" + @echo "PUGIXML_SUBDIR: $(PUGIXML_SUBDIR)" + @echo "ZIPPY_SUBDIR: $(ZIPPY_SUBDIR)" + @echo +# @echo "OBJS_LICENSE: $(OBJS_LICENSE)" +# @echo "OBJS_SHARED: $(OBJS_SHARED)" + @echo "OBJS_PUGIXML: $(OBJS_PUGIXML)" + @echo "OBJS_ZIPPY: $(OBJS_ZIPPY)" + @echo "OBJS_OPENXLSX: $(OBJS_OPENXLSX)" + @echo "OBJS_OPENXLSX_PREFIXED: $(OBJS_OPENXLSX_PREFIXED)" + @echo "OBJS_DEMOS: $(OBJS_DEMOS)" + @echo + @echo "PROJECT_FLAGS: $(PROJECT_FLAGS)" + @echo "ADDITIONAL_INCLUDE_FLAGS: $(ADDITIONAL_INCLUDE_FLAGS)" + @echo + @echo "OBJS_DEMO1: $(OBJS_DEMO1)" + @echo "OBJS_OPENXLSX_DEMO1: $(OBJS_OPENXLSX_DEMO1)" + @echo "LDLIBS_DEMO1: $(LDLIBS_DEMO1)" + @echo "OBJS_DEMO2: $(OBJS_DEMO2)" + @echo "OBJS_OPENXLSX_DEMO2: $(OBJS_OPENXLSX_DEMO2)" + @echo "LDLIBS_DEMO2: $(LDLIBS_DEMO2)" + @echo "OBJS_DEMO3: $(OBJS_DEMO3)" + @echo "OBJS_OPENXLSX_DEMO3: $(OBJS_OPENXLSX_DEMO3)" + @echo "LDLIBS_DEMO3: $(LDLIBS_DEMO3)" + @echo "OBJS_DEMO4: $(OBJS_DEMO4)" + @echo "OBJS_OPENXLSX_DEMO4: $(OBJS_OPENXLSX_DEMO4)" + @echo "LDLIBS_DEMO4: $(LDLIBS_DEMO4)" + @echo "OBJS_DEMO5: $(OBJS_DEMO5)" + @echo "OBJS_OPENXLSX_DEMO5: $(OBJS_OPENXLSX_DEMO5)" + @echo "LDLIBS_DEMO5: $(LDLIBS_DEMO5)" + @echo "OBJS_DEMO6: $(OBJS_DEMO6)" + @echo "OBJS_OPENXLSX_DEMO6: $(OBJS_OPENXLSX_DEMO6)" + @echo "LDLIBS_DEMO6: $(LDLIBS_DEMO6)" + @echo "OBJS_DEMO7: $(OBJS_DEMO7)" + @echo "OBJS_OPENXLSX_DEMO7: $(OBJS_OPENXLSX_DEMO7)" + @echo "LDLIBS_DEMO7: $(LDLIBS_DEMO7)" + @echo "OBJS_DEMO8: $(OBJS_DEMO8)" + @echo "OBJS_OPENXLSX_DEMO8: $(OBJS_OPENXLSX_DEMO8)" + @echo "LDLIBS_DEMO8: $(LDLIBS_DEMO8)" + @echo + @echo "SANITIZE_FLAGS: $(SANITIZE_FLAGS)" + @echo "SANITIZE_LIBS: $(SANITIZE_LIBS)" + @echo "DISABLED_WARNING_FLAGS: $(DISABLED_WARNING_FLAGS)" + @echo "GENERIC_FLAGS: $(GENERIC_FLAGS)" + @echo "CPPFLAGS: $(CPPFLAGS)" + @echo + @echo "LDFLAGS: $(LDFLAGS)" + @echo "LDLIBS: $(LDLIBS)" diff --git a/OpenXLSX/CMakeLists.txt b/OpenXLSX/CMakeLists.txt index 5385134..5aa7871 100644 --- a/OpenXLSX/CMakeLists.txt +++ b/OpenXLSX/CMakeLists.txt @@ -108,6 +108,7 @@ set(OPENXLSX_SOURCES ${CMAKE_CURRENT_LIST_DIR}/sources/XLWorkbook.cpp ${CMAKE_CURRENT_LIST_DIR}/sources/XLXmlData.cpp ${CMAKE_CURRENT_LIST_DIR}/sources/XLXmlFile.cpp + ${CMAKE_CURRENT_LIST_DIR}/sources/XLXmlParser.cpp ${CMAKE_CURRENT_LIST_DIR}/sources/XLZipArchive.cpp ) @@ -116,7 +117,7 @@ set(OPENXLSX_SOURCES # STATIC AND SHARED LIBRARY # Check that the input is valid #======================================================================================================================= -if(NOT ${OPENXLSX_LIBRARY_TYPE} STREQUAL "STATIC" AND NOT ${OPENXLSX_LIBRARY_TYPE} STREQUAL "SHARED") +if(NOT "${OPENXLSX_LIBRARY_TYPE}" STREQUAL "STATIC" AND NOT "${OPENXLSX_LIBRARY_TYPE}" STREQUAL "SHARED") message( FATAL_ERROR "Invalid library type. Must be SHARED or STATIC." ) endif() @@ -124,7 +125,7 @@ endif() # STATIC LIBRARY # Define the static library #======================================================================================================================= -if (${OPENXLSX_LIBRARY_TYPE} STREQUAL "STATIC") +if ("${OPENXLSX_LIBRARY_TYPE}" STREQUAL "STATIC") add_library(OpenXLSX STATIC "") add_library(OpenXLSX::OpenXLSX ALIAS OpenXLSX) target_sources(OpenXLSX PRIVATE ${OPENXLSX_SOURCES}) @@ -152,7 +153,7 @@ endif () # SHARED LIBRARY # Define the shared library #======================================================================================================================= -if (${OPENXLSX_LIBRARY_TYPE} STREQUAL "SHARED") +if ("${OPENXLSX_LIBRARY_TYPE}" STREQUAL "SHARED") add_library(OpenXLSX SHARED "") add_library(OpenXLSX::OpenXLSX ALIAS OpenXLSX) target_sources(OpenXLSX PRIVATE ${OPENXLSX_SOURCES}) diff --git a/OpenXLSX/OpenXLSX.hpp b/OpenXLSX/OpenXLSX.hpp index 77cb1bf..568a90b 100644 --- a/OpenXLSX/OpenXLSX.hpp +++ b/OpenXLSX/OpenXLSX.hpp @@ -60,4 +60,4 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. #include "headers/XLWorkbook.hpp" #include "headers/XLZipArchive.hpp" -#endif // OPENXLSX_OPENXLSX_HPP \ No newline at end of file +#endif // OPENXLSX_OPENXLSX_HPP diff --git a/OpenXLSX/external/nowide/nowide/args.hpp b/OpenXLSX/external/nowide/args.hpp similarity index 100% rename from OpenXLSX/external/nowide/nowide/args.hpp rename to OpenXLSX/external/nowide/args.hpp diff --git a/OpenXLSX/external/nowide/nowide/cenv.hpp b/OpenXLSX/external/nowide/cenv.hpp similarity index 100% rename from OpenXLSX/external/nowide/nowide/cenv.hpp rename to OpenXLSX/external/nowide/cenv.hpp diff --git a/OpenXLSX/external/nowide/nowide/config.hpp b/OpenXLSX/external/nowide/config.hpp similarity index 100% rename from OpenXLSX/external/nowide/nowide/config.hpp rename to OpenXLSX/external/nowide/config.hpp diff --git a/OpenXLSX/external/nowide/nowide/convert.hpp b/OpenXLSX/external/nowide/convert.hpp similarity index 100% rename from OpenXLSX/external/nowide/nowide/convert.hpp rename to OpenXLSX/external/nowide/convert.hpp diff --git a/OpenXLSX/external/nowide/nowide/cstdio.hpp b/OpenXLSX/external/nowide/cstdio.hpp similarity index 100% rename from OpenXLSX/external/nowide/nowide/cstdio.hpp rename to OpenXLSX/external/nowide/cstdio.hpp diff --git a/OpenXLSX/external/nowide/nowide/cstdlib.hpp b/OpenXLSX/external/nowide/cstdlib.hpp similarity index 100% rename from OpenXLSX/external/nowide/nowide/cstdlib.hpp rename to OpenXLSX/external/nowide/cstdlib.hpp diff --git a/OpenXLSX/external/nowide/nowide/encoding_errors.hpp b/OpenXLSX/external/nowide/encoding_errors.hpp similarity index 100% rename from OpenXLSX/external/nowide/nowide/encoding_errors.hpp rename to OpenXLSX/external/nowide/encoding_errors.hpp diff --git a/OpenXLSX/external/nowide/nowide/encoding_utf.hpp b/OpenXLSX/external/nowide/encoding_utf.hpp similarity index 100% rename from OpenXLSX/external/nowide/nowide/encoding_utf.hpp rename to OpenXLSX/external/nowide/encoding_utf.hpp diff --git a/OpenXLSX/external/nowide/nowide/filebuf.hpp b/OpenXLSX/external/nowide/filebuf.hpp similarity index 100% rename from OpenXLSX/external/nowide/nowide/filebuf.hpp rename to OpenXLSX/external/nowide/filebuf.hpp diff --git a/OpenXLSX/external/nowide/nowide/fstream.hpp b/OpenXLSX/external/nowide/fstream.hpp similarity index 100% rename from OpenXLSX/external/nowide/nowide/fstream.hpp rename to OpenXLSX/external/nowide/fstream.hpp diff --git a/OpenXLSX/external/nowide/nowide/iostream.hpp b/OpenXLSX/external/nowide/iostream.hpp similarity index 100% rename from OpenXLSX/external/nowide/nowide/iostream.hpp rename to OpenXLSX/external/nowide/iostream.hpp diff --git a/OpenXLSX/external/nowide/nowide/scoped_ptr.hpp b/OpenXLSX/external/nowide/scoped_ptr.hpp similarity index 100% rename from OpenXLSX/external/nowide/nowide/scoped_ptr.hpp rename to OpenXLSX/external/nowide/scoped_ptr.hpp diff --git a/OpenXLSX/external/nowide/nowide/stackstring.hpp b/OpenXLSX/external/nowide/stackstring.hpp similarity index 100% rename from OpenXLSX/external/nowide/nowide/stackstring.hpp rename to OpenXLSX/external/nowide/stackstring.hpp diff --git a/OpenXLSX/external/nowide/nowide/system.hpp b/OpenXLSX/external/nowide/system.hpp similarity index 100% rename from OpenXLSX/external/nowide/nowide/system.hpp rename to OpenXLSX/external/nowide/system.hpp diff --git a/OpenXLSX/external/nowide/nowide/utf.hpp b/OpenXLSX/external/nowide/utf.hpp similarity index 100% rename from OpenXLSX/external/nowide/nowide/utf.hpp rename to OpenXLSX/external/nowide/utf.hpp diff --git a/OpenXLSX/external/nowide/nowide/utf8_codecvt.hpp b/OpenXLSX/external/nowide/utf8_codecvt.hpp similarity index 100% rename from OpenXLSX/external/nowide/nowide/utf8_codecvt.hpp rename to OpenXLSX/external/nowide/utf8_codecvt.hpp diff --git a/OpenXLSX/external/nowide/nowide/windows.hpp b/OpenXLSX/external/nowide/windows.hpp similarity index 100% rename from OpenXLSX/external/nowide/nowide/windows.hpp rename to OpenXLSX/external/nowide/windows.hpp diff --git a/OpenXLSX/external/pugixml/pugixml.cpp b/OpenXLSX/external/pugixml/pugixml.cpp index 721bfe0..7fc66be 100644 --- a/OpenXLSX/external/pugixml/pugixml.cpp +++ b/OpenXLSX/external/pugixml/pugixml.cpp @@ -14,8 +14,6 @@ #ifndef SOURCE_PUGIXML_CPP #define SOURCE_PUGIXML_CPP -#include - #include "pugixml.hpp" #include @@ -5705,64 +5703,6 @@ namespace pugi return xml_node(); } - /* BEGIN 2024-04-26 Lars Uffmann: added next_sibling_of_type, previous_sibling_of_type */ - PUGI_IMPL_FN xml_node xml_node::next_sibling_of_type(xml_node_type t) const - { - if (_root) { - xml_node_struct* next = _root->next_sibling; - // while (next && (xml_node(next).type() != t)) next = next->next_sibling; - while (next && (PUGI_IMPL_NODETYPE(next) != t)) next = next->next_sibling; // faster than creating an xml_node object to access the .type() method? - if( next ) - return xml_node(next); - } - return xml_node(); // if no node matching type t was found: return an empty node - } - - PUGI_IMPL_FN xml_node xml_node::previous_sibling_of_type(xml_node_type t) const - { - // return prev->next_sibling ? xml_node(prev) : xml_node(); - if (_root) { - xml_node_struct* prev = _root->prev_sibling_c; - // while (prev->next_sibling && (xml_node(prev).type() != t)) prev = prev->prev_sibling_c; - while (prev->next_sibling && (PUGI_IMPL_NODETYPE(prev) != t)) prev = prev->prev_sibling_c; // faster than creating an xml_node object to access the .type() method? - - if( prev->next_sibling ) - return xml_node(prev); - } - return xml_node(); // if no node matching type t was found: return an empty node - } - - PUGI_IMPL_FN xml_node xml_node::next_sibling_of_type(const char_t* name_, xml_node_type t) const - { - if (_root) { - for (xml_node_struct* i = _root->next_sibling; i; i = i->next_sibling) - { - const char_t* iname = i->name; - // if (iname && impl::strequal(name_, iname) && (xml_node(i).type() == t)) - if (iname && impl::strequal(name_, iname) && (PUGI_IMPL_NODETYPE(i) == t)) // faster than creating an xml_node object to access the .type() method? - return xml_node(i); - } - } - - return xml_node(); // if no node matching type t was found: return an empty node - } - - PUGI_IMPL_FN xml_node xml_node::previous_sibling_of_type(const char_t* name_, xml_node_type t) const - { - if (_root) { - for (xml_node_struct* i = _root->prev_sibling_c; i->next_sibling; i = i->prev_sibling_c) - { - const char_t* iname = i->name; - // if (iname && impl::strequal(name_, iname) && (xml_node(i).type() == t)) - if (iname && impl::strequal(name_, iname) && (PUGI_IMPL_NODETYPE(i) == t)) // faster than creating an xml_node object to access the .type() method? - return xml_node(i); - } - } - return xml_node(); // if no node matching type t was found: return an empty node - } - /* END 2024-04-26 Lars Uffmann: added next_sibling_of_type, previous_sibling_of_type */ - - PUGI_IMPL_FN xml_attribute xml_node::attribute(const char_t* name_, xml_attribute& hint_) const { xml_attribute_struct* hint = hint_._attr; @@ -5873,47 +5813,6 @@ namespace pugi return first ? xml_node(first->prev_sibling_c) : xml_node(); } - /* BEGIN 2024-04-25 Lars Uffmann: added first_child_of_type, last_child_of_type */ - PUGI_IMPL_FN xml_node xml_node::first_child_of_type(xml_node_type t) const - { - if (_root) { - auto x = first_child(); - auto l = last_child(); - while(x != l && x.type() != t) x = x.next_sibling(); - if( x.type() == t ) - return xml_node(x); - } - return xml_node(); // if no node matching type t was found: return an empty node - } - - PUGI_IMPL_FN xml_node xml_node::last_child_of_type(xml_node_type t) const - { - if (_root) { - auto f = first_child(); - auto x = last_child(); - while (x != f && x.type() != t) x = x.previous_sibling(); - if( x.type() == t ) - return xml_node(x); - } - return xml_node(); // if no node matching type t was found: return an empty node - } - /* END 2024-04-25 Lars Uffmann: added first_child_of_type, last_child_of_type */ - - /* BEGIN 2024-04-28 Lars Uffmann: added child_count_of_type */ - PUGI_IMPL_FN size_t xml_node::child_count_of_type(xml_node_type type) const - { - size_t counter = 0; - if (_root) { - auto c = first_child_of_type( type ); - while( c ) { - ++counter; - c = c.next_sibling_of_type( type ); - } - } - return counter; - } - /* END 2024-04-28 Lars Uffmann: added child_count_of_type */ - PUGI_IMPL_FN bool xml_node::set_name(const char_t* rhs) { xml_node_type type_ = _root ? PUGI_IMPL_NODETYPE(_root) : node_null; @@ -7546,7 +7445,7 @@ namespace pugi { impl::xml_buffered_writer buffered_writer(writer, encoding); - if ((flags & format_write_bom) && encoding != encoding_latin1) + if ((flags & format_write_bom) && buffered_writer.encoding != encoding_latin1) { // BOM always represents the codepoint U+FEFF, so just write it in native encoding #ifdef PUGIXML_WCHAR_MODE @@ -7560,7 +7459,7 @@ namespace pugi if (!(flags & format_no_declaration) && !impl::has_declaration(_root)) { buffered_writer.write_string(PUGIXML_TEXT("'); if (!(flags & format_raw)) buffered_writer.write('\n'); } diff --git a/OpenXLSX/external/pugixml/pugixml.hpp b/OpenXLSX/external/pugixml/pugixml.hpp index 2b91792..7ecda50 100644 --- a/OpenXLSX/external/pugixml/pugixml.hpp +++ b/OpenXLSX/external/pugixml/pugixml.hpp @@ -532,19 +532,6 @@ namespace pugi xml_node next_sibling() const; xml_node previous_sibling() const; - // 2024-04-25 Lars Uffmann: added first_child_of_type, last_child_of_type - xml_node first_child_of_type(xml_node_type t = node_element) const; - xml_node last_child_of_type(xml_node_type t = node_element) const; - - // 2024-04-28 Lars Uffmann: added child_count_of_type - size_t child_count_of_type(xml_node_type type = node_element) const; - - // 2024-04-26 Lars Uffmann: added next_sibling_of_type, previous_sibling_of_type - xml_node next_sibling_of_type(xml_node_type t = node_element) const; - xml_node previous_sibling_of_type(xml_node_type t = node_element) const; - xml_node next_sibling_of_type(const char_t* name_, xml_node_type t = node_element) const; - xml_node previous_sibling_of_type(const char_t* name_, xml_node_type t = node_element) const; - // Get parent node xml_node parent() const; diff --git a/OpenXLSX/external/zippy/zippy.hpp b/OpenXLSX/external/zippy/zippy.hpp index 7e49370..cd0703f 100644 --- a/OpenXLSX/external/zippy/zippy.hpp +++ b/OpenXLSX/external/zippy/zippy.hpp @@ -27,14 +27,14 @@ # include #endif -#ifdef ENABLE_NOWIDE // TODO TBD: test this on windows +#ifdef ENABLE_NOWIDE // DONE: test this on windows # include # define FILESYSTEM_NAMESPACE nowide #else # define FILESYSTEM_NAMESPACE std #endif -namespace +namespace ns_miniz { /* miniz.c 2.0.8 - public domain deflate/inflate, zlib-subset, ZIP reading/writing/appending, PNG writing See "unlicense" statement at the end of this file. @@ -9691,10 +9691,12 @@ handle_failure: #endif /*#ifndef MINIZ_NO_ARCHIVE_APIS*/ -} // namespace +} // namespace ns_miniz namespace Zippy { + using namespace ns_miniz; + /** * @brief The ZipRuntimeError class is a custom exception class derived from the std::runtime_error class. * @details In case of an error in the Zippy library, an ZipRuntimeError object will be thrown, with a message @@ -10752,7 +10754,7 @@ namespace Zippy // pull request #191, support AmigaOS style paths # ifdef __amigaos__ - constexpr const char * localFolder = "\"\"/"; // local folder on AmigaOS is "" + constexpr const char * localFolder = ""; // local folder on AmigaOS can not be explicitly expressed in a path if (pathPos == std::string::npos) pathPos = filename.rfind(':'); // if no '/' found, attempt to find amiga drive root path # else constexpr const char * localFolder = "./"; // local folder on _WIN32 && __linux__ is . @@ -10848,7 +10850,10 @@ namespace Zippy // ===== If data has not been extracted from the archive (i.e., m_EntryData is empty), // ===== extract the data from the archive to the ZipEntry object. if (result->m_EntryData.empty()) { - result->m_EntryData.resize(result->UncompressedSize()); + if (result->UncompressedSize()) + result->m_EntryData.resize(result->UncompressedSize()); + else + result->m_EntryData.resize(1); // 2024-06-03 BUFIX: std::vector::data() can be nullptr when ::size() is 0, leading to a failure to load an empty file mz_zip_reader_extract_file_to_mem(&m_Archive, name.c_str(), result->m_EntryData.data(), result->m_EntryData.size(), 0); } diff --git a/OpenXLSX/headers/XLCell.hpp b/OpenXLSX/headers/XLCell.hpp index a7cb84d..7fd7c28 100644 --- a/OpenXLSX/headers/XLCell.hpp +++ b/OpenXLSX/headers/XLCell.hpp @@ -50,6 +50,8 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. #pragma warning(disable : 4251) #pragma warning(disable : 4275) +#include // std::ostream +#include // std::basic_ostream #include // ===== OpenXLSX Includes ===== // @@ -217,11 +219,25 @@ namespace OpenXLSX XLCellAssignable() : XLCell() {} /** - * @brief Inherit all constructors with parameters from XLCell + * @brief Copy constructor. Constructs an assignable XLCell from an existing cell + * @param other the cell to construct from */ - template - explicit XLCellAssignable(base b) : XLCell(b) - {} + XLCellAssignable (XLCell const & other); + + /** + * @brief Move constructor. Constructs an assignable XLCell from a temporary (r)value + * @param other the cell to construct from + */ + XLCellAssignable (XLCell && other); + + // /** + // * @brief Inherit all constructors with parameters from XLCell + // */ + // template + // // explicit XLCellAssignable(base b) : XLCell(b) + // // NOTE: BUG: explicit keyword triggers tons of compiler errors when << operator attempts to use an XLCell (implicit conversion works because << is overloaded for XLCellAssignable) + // XLCellAssignable(base b) : XLCell(b) + // {} /** * @brief Copy assignment operator @@ -263,7 +279,20 @@ namespace OpenXLSX inline bool operator!=(const XLCell& lhs, const XLCell& rhs) { return !XLCell::isEqual(lhs, rhs); } /** - * @brief + * @brief ostream output of XLCell content as string + * @param os the ostream destination + * @param c the cell to output to the stream + * @return + */ + inline std::ostream& operator<<(std::ostream& os, const XLCell& c) + { + os << c.getString(); + // TODO: send to stream different data types based on cell data type + return os; + } + + /** + * @brief ostream output of XLCellAssignable content as string * @param os the ostream destination * @param c the cell to output to the stream * @return diff --git a/OpenXLSX/headers/XLCellIterator.hpp b/OpenXLSX/headers/XLCellIterator.hpp index e4df787..66c87ea 100644 --- a/OpenXLSX/headers/XLCellIterator.hpp +++ b/OpenXLSX/headers/XLCellIterator.hpp @@ -145,6 +145,12 @@ namespace OpenXLSX */ bool operator!=(const XLCellIterator& rhs) const; + /** + * @brief determine whether iterator is at 1 beyond the last cell in range + * @return + */ + const bool endReached() const { return m_endReached; } + /** * @brief * @param last @@ -152,15 +158,32 @@ namespace OpenXLSX */ uint64_t distance(const XLCellIterator& last); + /** + * @brief get the XLCellReference::address corresponding to the current iterator position + * @return an XLCellReference::address, with m_bottomRight.col() + 1 for the beyond-the-end iterator + */ + const std::string address() const; + private: std::unique_ptr m_dataNode; /**< */ XLCellReference m_topLeft; /**< The cell reference of the first cell in the range */ XLCellReference m_bottomRight; /**< The cell reference of the last cell in the range */ XLCell m_currentCell; /**< */ XLSharedStrings m_sharedStrings; /**< */ - bool m_endReached { false }; /**< */ + bool m_endReached; /**< */ }; + /** + * @brief ostream output of XLIterator position as XLCellReference::address + * @param os the ostream destination + * @param it the XLIterator whose position to send to the stream + * @return + */ + inline std::ostream& operator<<(std::ostream& os, const XLCellIterator& it) + { + os << it.address(); + return os; + } } // namespace OpenXLSX // ===== Template specialization for std::distance. diff --git a/OpenXLSX/headers/XLCellRange.hpp b/OpenXLSX/headers/XLCellRange.hpp index d357baa..098f2ee 100644 --- a/OpenXLSX/headers/XLCellRange.hpp +++ b/OpenXLSX/headers/XLCellRange.hpp @@ -166,4 +166,4 @@ namespace OpenXLSX } // namespace OpenXLSX #pragma warning(pop) -#endif // OPENXLSX_XLCELLRANGE_HPP \ No newline at end of file +#endif // OPENXLSX_XLCELLRANGE_HPP diff --git a/OpenXLSX/headers/XLCellValue.hpp b/OpenXLSX/headers/XLCellValue.hpp index e5068ba..1ee2cfc 100644 --- a/OpenXLSX/headers/XLCellValue.hpp +++ b/OpenXLSX/headers/XLCellValue.hpp @@ -284,7 +284,7 @@ namespace OpenXLSX try { return std::visit(VisitXLCellValueTypeToString(), m_value); } - catch (std::string s) { + catch (...) { // 2024-05-27: was catch( string s ) - must have been a typo, currently nothing throws a string here throw XLValueTypeError("XLCellValue object is not convertible to string."); } } diff --git a/OpenXLSX/headers/XLCommandQuery.hpp b/OpenXLSX/headers/XLCommandQuery.hpp index e10de39..a625fe5 100644 --- a/OpenXLSX/headers/XLCommandQuery.hpp +++ b/OpenXLSX/headers/XLCommandQuery.hpp @@ -52,6 +52,7 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. // ===== External Includes ===== // #include +#include // uint8_t #include #include @@ -67,6 +68,8 @@ namespace OpenXLSX SetSheetIndex, SetSheetActive, ResetCalcChain, + CheckAndFixCoreProperties, + CheckAndFixExtendedProperties, AddSharedStrings, AddWorksheet, AddChartsheet, diff --git a/OpenXLSX/headers/XLContentTypes.hpp b/OpenXLSX/headers/XLContentTypes.hpp index 2f429ca..8e86f2d 100644 --- a/OpenXLSX/headers/XLContentTypes.hpp +++ b/OpenXLSX/headers/XLContentTypes.hpp @@ -51,6 +51,7 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. #pragma warning(disable : 4275) // ===== External Includes ===== // +#include // uint8_t #include #include #include diff --git a/OpenXLSX/headers/XLDocument.hpp b/OpenXLSX/headers/XLDocument.hpp index a9052b0..508db64 100644 --- a/OpenXLSX/headers/XLDocument.hpp +++ b/OpenXLSX/headers/XLDocument.hpp @@ -51,6 +51,8 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. #pragma warning(disable : 4275) // ===== External Includes ===== // +#include // std::find_if +#include #include // ===== OpenXLSX Includes ===== // @@ -65,8 +67,6 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. #include "XLXmlData.hpp" #include "XLZipArchive.hpp" -#include - namespace OpenXLSX { /** @@ -189,8 +189,9 @@ namespace OpenXLSX /** * @brief Get the filename of the current document, e.g. "spreadsheet.xlsx". * @return A std::string with the filename. + * @note 2024-06-03: function can't return as reference to const because filename as a substr of m_filePath can be a temporary */ - const std::string& name() const; + const std::string name() const; /** * @brief Get the full path of the current document, e.g. "drive/blah/spreadsheet.xlsx" diff --git a/OpenXLSX/headers/XLException.hpp b/OpenXLSX/headers/XLException.hpp index 4e424b2..136658d 100644 --- a/OpenXLSX/headers/XLException.hpp +++ b/OpenXLSX/headers/XLException.hpp @@ -152,4 +152,4 @@ namespace OpenXLSX } // namespace OpenXLSX #pragma warning(pop) -#endif // OPENXLSX_XLEXCEPTION_HPP \ No newline at end of file +#endif // OPENXLSX_XLEXCEPTION_HPP diff --git a/OpenXLSX/headers/XLFormula.hpp b/OpenXLSX/headers/XLFormula.hpp index 9dc097a..221084b 100644 --- a/OpenXLSX/headers/XLFormula.hpp +++ b/OpenXLSX/headers/XLFormula.hpp @@ -225,9 +225,9 @@ namespace OpenXLSX */ template< typename T, - typename = std::enable_if_t, XLFormula> || std::is_same_v, std::string> || - std::is_same_v, std::string_view> || std::is_same_v, const char*> || - std::is_same_v, char*>>> + typename = std::enable_if_t, XLFormula> || std::is_same_v, std::string> || + std::is_same_v, std::string_view> || std::is_same_v, const char*> || + std::is_same_v, char*>>> XLFormulaProxy& operator=(T formula) { if constexpr (std::is_same_v, XLFormula>) diff --git a/OpenXLSX/headers/XLProperties.hpp b/OpenXLSX/headers/XLProperties.hpp index 67165d4..f080a05 100644 --- a/OpenXLSX/headers/XLProperties.hpp +++ b/OpenXLSX/headers/XLProperties.hpp @@ -64,6 +64,13 @@ namespace OpenXLSX */ class OPENXLSX_EXPORT XLProperties : public XLXmlFile { + private: + /** + * @brief constructor helper function: create core.xml content from template + * @param workbook + */ + void createFromTemplate(); + //---------------------------------------------------------------------------------------------------------------------- // Public Member Functions //---------------------------------------------------------------------------------------------------------------------- @@ -159,6 +166,13 @@ namespace OpenXLSX */ class OPENXLSX_EXPORT XLAppProperties : public XLXmlFile { + private: + /** + * @brief constructor helper function: create app.xml content from template + * @param workbook + */ + void createFromTemplate(XMLDocument const & workbookXml); + //-------------------------------------------------------------------------------------------------------------- // Public Member Functions //-------------------------------------------------------------------------------------------------------------- @@ -169,6 +183,13 @@ namespace OpenXLSX */ XLAppProperties() = default; + /** + * @brief enable XLAppProperties to re-create a worksheet list in docProps/app.xml element from workbookXml + * @param xmlData + * @param workbook + */ + explicit XLAppProperties(XLXmlData* xmlData, XMLDocument const & workbookXml); + /** * @brief * @param xmlData diff --git a/OpenXLSX/headers/XLRelationships.hpp b/OpenXLSX/headers/XLRelationships.hpp index d0d4b8a..faeaf6b 100644 --- a/OpenXLSX/headers/XLRelationships.hpp +++ b/OpenXLSX/headers/XLRelationships.hpp @@ -94,7 +94,18 @@ namespace OpenXLSX ControlProperties, Unknown }; +} // namespace OpenXLSX +namespace OpenXLSX_XLRelationships { // special namespace to avoid naming conflict with another GetStringFromType function + using namespace OpenXLSX; + /** + * @brief helper function, used only within module and from XLProperties.cpp / XLAppProperties::createFromTemplate + * @param type the XLRelationshipType for which to return the correct XML string + */ + std::string GetStringFromType(XLRelationshipType type); +} // namespace OpenXLSX_XLRelationships + +namespace OpenXLSX { /** * @brief An encapsulation of a relationship item, i.e. an XML file in the document, its type and an ID number. */ diff --git a/OpenXLSX/headers/XLSharedStrings.hpp b/OpenXLSX/headers/XLSharedStrings.hpp index cc5317a..c52a0d9 100644 --- a/OpenXLSX/headers/XLSharedStrings.hpp +++ b/OpenXLSX/headers/XLSharedStrings.hpp @@ -51,6 +51,8 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. #pragma warning(disable : 4275) #include +#include // std::numeric_limits +#include // std::basic_ostream #include // ===== OpenXLSX Includes ===== // @@ -59,6 +61,8 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. namespace OpenXLSX { + constexpr size_t XLMaxSharedStrings = std::numeric_limits< int32_t >::max(); + /** * @brief This class encapsulate the Excel concept of Shared Strings. In Excel, instead of havig individual strings * in each cell, cells have a reference to an entry in the SharedStrings register. This results in smalle file @@ -133,12 +137,12 @@ namespace OpenXLSX * @param index * @return */ - const char* getString(uint32_t index) const; + const char* getString(int32_t index) const; /** * @brief Append a new string to the list of shared strings. * @param str The string to append. - * @return A long int with the index of the appended string + * @return An int32_t with the index of the appended string */ int32_t appendString(const std::string& str); @@ -149,12 +153,20 @@ namespace OpenXLSX * shared string indices for the cells in the spreadsheet. Instead use this member functions, which clears * the contents of the string, but keeps the XMLNode holding the string. */ - void clearString(uint64_t index); + void clearString(int32_t index); + + // 2024-06-18 TBD if this is ever needed + // /** + // * @brief check m_stringCache is initialized + // * @return true if m_stringCache != nullptr, false otherwise + // * @note 2024-05-28 added function to enable other classes to check m_stringCache status + // */ + // bool initialized() const { return m_stringCache != nullptr; } /** * @brief print the XML contents of the shared strings document using the underlying XMLNode print function */ - void print(std::basic_ostream>& ostr); + void print(std::basic_ostream& ostr) const; private: std::deque* m_stringCache {}; /** < Each string must have an unchanging memory address; hence the use of std::deque */ diff --git a/OpenXLSX/headers/XLSheet.hpp b/OpenXLSX/headers/XLSheet.hpp index 93e8c9e..e08e22a 100644 --- a/OpenXLSX/headers/XLSheet.hpp +++ b/OpenXLSX/headers/XLSheet.hpp @@ -51,6 +51,8 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. #pragma warning(disable : 4275) // ===== External Includes ===== // +#include // uint8_t, uint16_t, uint32_t +#include // std::basic_ostream #include #include @@ -730,7 +732,7 @@ namespace OpenXLSX /** * @brief print the XML contents of the XLSheet using the underlying XMLNode print function */ - void print(std::basic_ostream >& ostr); + void print(std::basic_ostream& ostr) const; //---------------------------------------------------------------------------------------------------------------------- // Private Member Variables diff --git a/OpenXLSX/headers/XLWorkbook.hpp b/OpenXLSX/headers/XLWorkbook.hpp index ab2c50e..91d1f99 100644 --- a/OpenXLSX/headers/XLWorkbook.hpp +++ b/OpenXLSX/headers/XLWorkbook.hpp @@ -51,6 +51,7 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. #pragma warning(disable : 4275) // ===== External Includes ===== // +#include // std::basic_ostream #include // ===== OpenXLSX Includes ===== // @@ -313,7 +314,7 @@ namespace OpenXLSX /** * @brief print the XML contents of the workbook.xml using the underlying XMLNode print function */ - void print(std::basic_ostream>& os); + void print(std::basic_ostream& ostr) const; private: // ---------- Private Member Functions ---------- // /** diff --git a/OpenXLSX/headers/XLXmlParser.hpp b/OpenXLSX/headers/XLXmlParser.hpp index b867270..87351bf 100644 --- a/OpenXLSX/headers/XLXmlParser.hpp +++ b/OpenXLSX/headers/XLXmlParser.hpp @@ -46,22 +46,143 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. #ifndef OPENXLSX_XLXMLPARSER_HPP #define OPENXLSX_XLXMLPARSER_HPP -namespace pugi -{ - class xml_node; - class xml_attribute; - class xml_document; -} // namespace pugi +// ===== pugixml.hpp needed for pugi::impl::xml_memory_page_type_mask, pugi::xml_node_type, pugi::char_t, pugi::node_element, pugi::xml_node, pugi::xml_attribute, pugi::xml_document +#include // not sure why the full include path is needed within the header file + +// 2024-05-29: forward declarations now pointless with include on pugixml being needed anyways +// namespace pugi +// { +// class xml_node; +// class xml_attribute; +// class xml_document; +// } // namespace pugi namespace OpenXLSX { - // class OpenXLSX_xml_node : public pugi::xml_node { - // using xml_node::xml_node; // use all constructors of pugi::xml_node - // TBD: can first_child_element and last_child_element be implemented here to skip whitespace nodes? - // }; - // using XMLNode = OpenXLSX_xml_node; - using XMLNode = pugi::xml_node; - using XMLAttribute = pugi::xml_attribute; - using XMLDocument = pugi::xml_document; + // ===== Copy definition of PUGI_IMPL_NODETYPE, which is defined in pugixml.cpp, within a namespace, and somehow doesn't work here +# define PUGI_IMPL_NODETYPE(n) static_cast((n)->header & pugi::impl::xml_memory_page_type_mask) + + + // disable this line to use original (non-augmented) pugixml +# define PUGI_AUGMENTED + + // ===== Using statements to switch between pugixml and augmented pugixml implementation +# ifdef PUGI_AUGMENTED + // ===== Forward declarations for using statements below + class OpenXLSX_xml_node; + class OpenXLSX_xml_document; + + using XMLNode = OpenXLSX_xml_node; + using XMLAttribute = pugi::xml_attribute; + using XMLDocument = OpenXLSX_xml_document; +# else + using XMLNode = pugi::xml_node; + using XMLAttribute = pugi::xml_attribute; + using XMLDocument = pugi::xml_document; +# endif + + // ===== Custom OpenXLSX_xml_node to add functionality to pugi::xml_node + class OpenXLSX_xml_node : public pugi::xml_node { + public: + /** + * @brief Default constructor. Constructs a null object. + */ + OpenXLSX_xml_node() : pugi::xml_node() {} + + /** + * @brief Inherit all constructors with parameters from pugi::xml_node + */ + template + // explicit OpenXLSX_xml_node(base b) : xml_node(b) // TBD + OpenXLSX_xml_node(base b) : pugi::xml_node(b) + {} + + // ===== BEGIN: Wrappers for xml_node member functions to ensure OpenXLSX_xml_node return values + // ===== CAUTION: this section is incomplete, only implementing those functions actually used by OpenXLSX to date + /** + * @brief for all functions: invoke the base class function, but with a return type of OpenXLSX_xml_node + */ + XMLNode parent() { return pugi::xml_node::parent(); } + XMLNode child(const pugi::char_t* name) const { return pugi::xml_node::child(name); } + template XMLNode find_child(Predicate pred) const { return pugi::xml_node::find_child(pred); } + // ===== END: Wrappers for xml_node member functions + + /** + * @brief get first node child that matches type + * @param type_ the pugi::xml_node_type to match + * @return a valid child matching the node type or an empty XMLNode + */ + XMLNode first_child_of_type(pugi::xml_node_type type_ = pugi::node_element) const; + + /** + * @brief get last node child that matches type + * @param type_ the pugi::xml_node_type to match + * @return a valid child matching the node type or an empty XMLNode + */ + XMLNode last_child_of_type(pugi::xml_node_type type_ = pugi::node_element) const; + + /** + * @brief count node children that match type + * @param type_ the pugi::xml_node_type to match + * @return the amount of node children matching type + */ + size_t child_count_of_type(pugi::xml_node_type type_ = pugi::node_element) const; + + /** + * @brief get next node sibling that matches type + * @param type_ the pugi::xml_node_type to match + * @return a valid sibling matching the node type or an empty XMLNode + */ + XMLNode next_sibling_of_type(pugi::xml_node_type type_ = pugi::node_element) const; + + /** + * @brief get previous node sibling that matches type + * @param type_ the pugi::xml_node_type to match + * @return a valid sibling matching the node type or an empty XMLNode + */ + XMLNode previous_sibling_of_type(pugi::xml_node_type type_ = pugi::node_element) const; + + /** + * @brief get next node sibling that matches name_ and type + * @param name_ the xml_node::name() to match + * @param type_ the pugi::xml_node_type to match + * @return a valid sibling matching the node type or an empty XMLNode + */ + XMLNode next_sibling_of_type(const pugi::char_t* name_, pugi::xml_node_type type_ = pugi::node_element) const; + + /** + * @brief get previous node sibling that matches name_ and type + * @param name_ the xml_node::name() to match + * @param type_ the pugi::xml_node_type to match + * @return a valid sibling matching the node type or an empty XMLNode + */ + XMLNode previous_sibling_of_type(const pugi::char_t* name_, pugi::xml_node_type type_ = pugi::node_element) const; + }; + + // ===== Custom OpenXLSX_xml_document to override relevant pugi::xml_document member functions with OpenXLSX_xml_node return value + class OpenXLSX_xml_document : public pugi::xml_document { + public: + /** + * @brief Default constructor. Constructs a null object. + */ + OpenXLSX_xml_document() : pugi::xml_document() {} + + /** + * @brief Inherit all constructors with parameters from pugi::xml_document + */ + template + // explicit OpenXLSX_xml_document(base b) : xml_document(b) // TBD + OpenXLSX_xml_document(base b) : pugi::xml_document(b) + {} + + // ===== BEGIN: Wrappers for xml_document member functions to ensure OpenXLSX_xml_node return values + // ===== CAUTION: this section is incomplete, only implementing those functions actually used by OpenXLSX to date + /** + * @brief for all functions: invoke the base class function, but with a return type of OpenXLSX_xml_node + */ + XMLNode document_element() const { return pugi::xml_document::document_element(); } + // ===== END: Wrappers for xml_document member functions + }; + } // namespace OpenXLSX #endif // OPENXLSX_XLXMLPARSER_HPP diff --git a/OpenXLSX/sources/XLCell.cpp b/OpenXLSX/sources/XLCell.cpp index bdceea3..6daf53b 100644 --- a/OpenXLSX/sources/XLCell.cpp +++ b/OpenXLSX/sources/XLCell.cpp @@ -136,8 +136,6 @@ void XLCell::copyFrom(XLCell const& other) using namespace std::literals::string_literals; if (!m_cellNode) { // copyFrom invoked by empty XLCell: create a new cell with reference & m_cellNode from other - std::cout << "copyFrom invoked by empty XLCell - creating a new cell with reference " << other.cellReference().address() - << std::endl; m_cellNode = std::make_unique(*other.m_cellNode); m_sharedStrings = other.m_sharedStrings; m_valueProxy = XLCellValueProxy(this, m_cellNode.get()); @@ -148,12 +146,14 @@ void XLCell::copyFrom(XLCell const& other) if ((&other != this) && (*other.m_cellNode == *m_cellNode)) // nothing to do return; + // ===== If m_cellNode points to a different XML node than other if ((&other != this) && (*other.m_cellNode != *m_cellNode)) { m_cellNode->remove_children(); - for (XMLNode child = other.m_cellNode->first_child(); !child.empty(); child = child.next_sibling()) m_cellNode->append_copy(child); - for (auto attr = m_cellNode->first_attribute(); !attr.empty(); attr = attr.next_attribute()) + // ===== Copy all XML attributes that are not the cell reference ("r") and all XML child nodes + for (XMLNode child = other.m_cellNode->first_child(); not child.empty(); child = child.next_sibling()) m_cellNode->append_copy(child); + for (auto attr = m_cellNode->first_attribute(); not attr.empty(); attr = attr.next_attribute()) if (strcmp(attr.name(), "r") != 0) m_cellNode->remove_attribute(attr); - for (auto attr = other.m_cellNode->first_attribute(); !attr.empty(); attr = attr.next_attribute()) + for (auto attr = other.m_cellNode->first_attribute(); not attr.empty(); attr = attr.next_attribute()) if (strcmp(attr.name(), "r") != 0) m_cellNode->append_copy(attr); } } @@ -161,7 +161,7 @@ void XLCell::copyFrom(XLCell const& other) /** * @details */ -XLCell::operator bool() const { return m_cellNode && *m_cellNode; } +XLCell::operator bool() const { return m_cellNode && (not m_cellNode->empty() ); } // ===== 2024-05-28: replaced explicit bool evaluation /** * @details This function returns a const reference to the cellReference property. @@ -197,6 +197,16 @@ XLFormulaProxy& XLCell::formula() { return m_formulaProxy; } */ void XLCell::print(std::basic_ostream& ostr) const { m_cellNode->print(ostr); } +/** + * @details + */ +XLCellAssignable::XLCellAssignable (XLCell const & other) : XLCell(other) {} + +/** + * @details + */ +XLCellAssignable::XLCellAssignable (XLCell && other) : XLCell(std::move(other)) {} + /** * @details */ diff --git a/OpenXLSX/sources/XLCellIterator.cpp b/OpenXLSX/sources/XLCellIterator.cpp index 0733c14..3b20d75 100644 --- a/OpenXLSX/sources/XLCellIterator.cpp +++ b/OpenXLSX/sources/XLCellIterator.cpp @@ -62,10 +62,13 @@ XLCellIterator::XLCellIterator(const XLCellRange& cellRange, XLIteratorLocation : m_dataNode(std::make_unique(*cellRange.m_dataNode)), m_topLeft(cellRange.m_topLeft), m_bottomRight(cellRange.m_bottomRight), - m_sharedStrings(cellRange.m_sharedStrings) + m_sharedStrings(cellRange.m_sharedStrings), + m_endReached(false) { - if (loc == XLIteratorLocation::End) + if (loc == XLIteratorLocation::End) { m_currentCell = XLCell(); + m_endReached = true; + } else { m_currentCell = XLCell(getCellNode(getRowNode(*m_dataNode, m_topLeft.row()), m_topLeft.column()), m_sharedStrings); } @@ -84,7 +87,8 @@ XLCellIterator::XLCellIterator(const XLCellIterator& other) m_topLeft(other.m_topLeft), m_bottomRight(other.m_bottomRight), m_currentCell(other.m_currentCell), - m_sharedStrings(other.m_sharedStrings) + m_sharedStrings(other.m_sharedStrings), + m_endReached(other.m_endReached) {} /** @@ -103,6 +107,7 @@ XLCellIterator& XLCellIterator::operator=(const XLCellIterator& other) m_bottomRight = other.m_bottomRight; m_currentCell = other.m_currentCell; m_sharedStrings = other.m_sharedStrings; + m_endReached = other.m_endReached; } return *this; @@ -118,6 +123,9 @@ XLCellIterator& XLCellIterator::operator=(XLCellIterator&& other) noexcept = def */ XLCellIterator& XLCellIterator::operator++() { + if (m_endReached) + throw XLInputError("XLCellIterator: tried to increment beyond end operator"); + auto ref = m_currentCell.cellReference(); // ===== Determine the cell reference for the next cell. @@ -128,11 +136,15 @@ XLCellIterator& XLCellIterator::operator++() else ref = XLCellReference(ref.row() + 1, m_topLeft.column()); + // 2024-06-03 TBD TODO: why ref > m_bottomRight - that shouldn't be possible? --> added exception to test for this + if (ref > m_bottomRight) + throw XLInternalError("XLCellIterator became > m_bottomRight - this should not happen!"); + if (m_endReached) m_currentCell = XLCell(); - else if (ref > m_bottomRight || ref.row() == m_currentCell.cellReference().row()) { + else if (ref > m_bottomRight || ref.row() == m_currentCell.cellReference().row()) { // TBD: remove ref > m_bottomRight condition unless I overlooked something auto node = m_currentCell.m_cellNode->next_sibling_of_type(pugi::node_element); - if (!node || XLCellReference(node.attribute("r").value()) != ref) { + if (node.empty() || XLCellReference(node.attribute("r").value()) != ref) { node = m_currentCell.m_cellNode->parent().insert_child_after("c", *m_currentCell.m_cellNode); node.append_attribute("r").set_value(ref.address().c_str()); } @@ -140,13 +152,12 @@ XLCellIterator& XLCellIterator::operator++() } else if (ref.row() > m_currentCell.cellReference().row()) { auto rowNode = m_currentCell.m_cellNode->parent().next_sibling_of_type(pugi::node_element); - if (!rowNode || rowNode.attribute("r").as_ullong() != ref.row()) { + if (rowNode.empty() || rowNode.attribute("r").as_ullong() != ref.row()) { rowNode = m_currentCell.m_cellNode->parent().parent().insert_child_after("row", m_currentCell.m_cellNode->parent()); rowNode.append_attribute("r").set_value(ref.row()); - // getRowNode(*m_dataNode, ref.row()); } - - m_currentCell = XLCell(getCellNode(rowNode, ref.column()), m_sharedStrings); + // ===== Pass the already known ref.row() to getCellNode so that it does not have to be fetched again + m_currentCell = XLCell(getCellNode(rowNode, ref.column(), ref.row()), m_sharedStrings); } else throw XLInternalError("An internal error occured"); @@ -191,14 +202,42 @@ bool XLCellIterator::operator!=(const XLCellIterator& rhs) const { return !(*thi /** * @details - * @todo This implementation is rather ineffecient. Consider an alternative implementation. + * @note 2024-06-03: implemented a calculated distance based on m_currentCell, m_topLeft and m_bottomRight (if m_endReached) + * accordingly, implemented defined setting of m_endReached at all times */ uint64_t XLCellIterator::distance(const XLCellIterator& last) { - uint64_t result = 0; - while (*this != last) { - ++result; - ++(*this); - } - return result; + // ===== Determine rows and columns, taking into account beyond-the-end iterators + uint32_t row = (m_endReached ? m_bottomRight.row() : m_currentCell.cellReference().row()); + uint16_t col = (m_endReached ? m_bottomRight.column() + 1 : m_currentCell.cellReference().column()); + uint32_t lastRow = (last.m_endReached ? last.m_bottomRight.row() : last.m_currentCell.cellReference().row()); + // ===== lastCol can store +1 for beyond-the-end iterator without overflow because MAX_COLS is less than max uint16_t + uint16_t lastCol = (last.m_endReached ? last.m_bottomRight.column() + 1 : last.m_currentCell.cellReference().column()); + + uint16_t rowWidth = m_bottomRight.column() - m_topLeft.column() + 1; // amount of cells in a row of the iterator range + int64_t distance = ((int64_t)(lastRow) - row) * rowWidth // row distance * rowWidth + + (int64_t)(lastCol) - col; // + column distance (may be negative) + if (distance < 0) + throw XLInputError("XLCellIterator::distance is negative"); + + return static_cast(distance); // after excluding negative result: cast back to positive value + + /* OBSOLETE CODE: + // uint64_t result = 0; + // while (*this != last) { + // ++result; + // ++(*this); + // } + // return result; + */ +} + +/** + * @details + */ +const std::string XLCellIterator::address() const +{ + uint32_t row = (m_endReached ? m_bottomRight.row() : m_currentCell.cellReference().row()); + uint16_t col = (m_endReached ? m_bottomRight.column() + 1 : m_currentCell.cellReference().column()); + return (m_endReached ? "END(" : "") + XLCellReference(row, col).address() + (m_endReached ? ")" : ""); } diff --git a/OpenXLSX/sources/XLCellReference.cpp b/OpenXLSX/sources/XLCellReference.cpp index 9c9dd80..e97bdcf 100644 --- a/OpenXLSX/sources/XLCellReference.cpp +++ b/OpenXLSX/sources/XLCellReference.cpp @@ -50,17 +50,13 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. # include #endif #include // pull requests #216, #232 +#include // std::isdigit // ===== OpenXLSX Includes ===== // #include "XLCellReference.hpp" #include "XLConstants.hpp" #include "XLException.hpp" -#include -#include -#include -#include - using namespace OpenXLSX; constexpr uint8_t alphabetSize = 26; @@ -81,13 +77,13 @@ namespace */ XLCellReference::XLCellReference(const std::string& cellAddress) { - if (!cellAddress.empty()) setAddress(cellAddress); - if (cellAddress.empty() || !addressIsValid(m_row, m_column)) { // 2024-04-25: throw exception on empty string + if (not cellAddress.empty()) setAddress(cellAddress); + if (cellAddress.empty() || not addressIsValid(m_row, m_column)) { // 2024-04-25: throw exception on empty string throw XLCellAddressError("Cell reference is invalid"); - // TODO below: possibly deprecated (if exception remains) - m_row = 1; - m_column = 1; - m_cellAddress = "A1"; + // ===== 2024-05-27: below code is obsolete due to exception on invalid cellAddress + // m_row = 1; + // m_column = 1; + // m_cellAddress = "A1"; } } @@ -182,7 +178,7 @@ XLCellReference& XLCellReference::operator--() else if (m_column == 1 && m_row == 1) { m_column = MAX_COLS; m_row = MAX_ROWS; - m_cellAddress = "XFD1048576"; + m_cellAddress = "XFD1048576"; // this address represents the very last cell that an excel spreadsheet can reference / support } return *this; } @@ -268,7 +264,7 @@ std::string XLCellReference::rowAsString(uint32_t row) { #ifdef CHARCONV_ENABLED std::array str {}; // NOLINT - const auto* p = std::to_chars(str.data(), str.data() + str.size(), row).ptr; + const auto* p = std::to_chars(str.data(), str.data() + str.size(), row).ptr; return std::string { str.data(), static_cast(p - str.data()) }; #else std::string result; @@ -292,10 +288,10 @@ uint32_t XLCellReference::rowAsNumber(const std::string& row) #ifdef CHARCONV_ENABLED uint32_t value = 0; std::from_chars(row.data(), row.data() + row.size(), value); // NOLINT -#else - uint32_t value = stoul(row); -#endif return value; +#else + return stoul(row); +#endif } /** @@ -314,11 +310,11 @@ std::string XLCellReference::columnAsString(uint16_t column) result += static_cast((column - (alphabetSize + 1)) % alphabetSize + asciiOffset + 1); } - // ===== If there is three letters in the Column Name: + // ===== If there are three letters in the Column Name: else { - result += char((column - 703) / (alphabetSize * alphabetSize) + asciiOffset + 1); // NOLINT - result += char(((column - 703) / alphabetSize) % alphabetSize + asciiOffset + 1); // NOLINT - result += char((column - 703) % alphabetSize + asciiOffset + 1); // NOLINT + result += static_cast((column - 703) / (alphabetSize * alphabetSize) + asciiOffset + 1); // NOLINT + result += static_cast(((column - 703) / alphabetSize) % alphabetSize + asciiOffset + 1); // NOLINT + result += static_cast((column - 703) % alphabetSize + asciiOffset + 1); // NOLINT } return result; @@ -326,48 +322,74 @@ std::string XLCellReference::columnAsString(uint16_t column) /** * @details Helper method to calculate the column number from column letter. + * @throws XLInputError + * @note 2024-06-03: added check for valid address */ uint16_t XLCellReference::columnAsNumber(const std::string& column) { - // uint16_t result = 0; - // - // for (int16_t i = static_cast(column.size() - 1), j = 0; i >= 0; --i, ++j) { // NOLINT - // result += static_cast((column[static_cast(i)] - asciiOffset) * std::pow(alphabetSize, j)); - // } - // - // return result; - uint16_t result = 0; - uint16_t factor = 1; - - for (int16_t i = static_cast(column.size() - 1); i >= 0; --i) { - result += static_cast((column[static_cast(i)] - asciiOffset) * factor); - factor *= alphabetSize; + uint64_t letterCount = 0; + uint32_t colNo = 0; + for (const auto letter : column) { + if (letter >= 'A' && letter <= 'Z') { // allow only uppercase letters + ++letterCount; + colNo = colNo * 26 + (letter - 'A' + 1); + } + else + break; } - return result; + // ===== If the full string was decoded and colNo is within allowed range [1;MAX_COLS] + if(letterCount == column.length() && colNo > 0 && colNo <= MAX_COLS) + return colNo; + throw XLInputError("XLCellReference::columnAsNumber - column \"" + column + "\" is invalid"); + + /* 2024-06-19 OBSOLETE CODE: + // uint16_t result = 0; + // uint16_t factor = 1; + // + // for (int16_t i = static_cast(column.size() - 1); i >= 0; --i) { + // result += static_cast((column[static_cast(i)] - asciiOffset) * factor); + // factor *= alphabetSize; + // } + // + // return result; + */ } /** * @details Helper method for calculating the coordinates from the cell address. - * @todo Consider checking if the given address is valid. + * @throws XLInputError + * @note 2024-06-03: added check for valid address */ XLCoordinates XLCellReference::coordinatesFromAddress(const std::string& address) { - // uint64_t letterCount = 0; - // for (const auto letter : address) { - // if (letter >= 65) // NOLINT - // ++letterCount; - // else if (letter <= 57) // NOLINT - // break; - // } - // - // const auto numberCount = address.size() - letterCount; - // - // return std::make_pair(rowAsNumber(address.substr(letterCount, numberCount)), columnAsNumber(address.substr(0, letterCount))); + uint64_t letterCount = 0; + uint32_t colNo = 0; + for (const auto letter : address) { + if (letter >= 'A' && letter <= 'Z') { // allow only uppercase letters + ++letterCount; + colNo = colNo * 26 + (letter - 'A' + 1); + } + else + break; + } - auto it = std::find_if(address.begin(), address.end(), ::isdigit); - auto columnPart = std::string(address.begin(), it); - auto rowPart = std::string(it, address.end()); + // ===== If address contains between 1 and 3 letters and has at least 1 more character for the row + if(colNo > 0 && colNo <= MAX_COLS && address.length() > letterCount) { + size_t pos = letterCount; + uint64_t rowNo = 0; + for (; pos < address.length() && std::isdigit(address[pos]); ++pos) // check digits + rowNo = rowNo * 10 + (address[pos] - '0'); + if (pos == address.length() && rowNo <= MAX_ROWS) // full address was < 4 letters + only digits + return std::make_pair(rowNo, colNo); + } + throw XLInputError("XLCellReference::coordinatesFromAddress - address \"" + address + "\" is invalid"); - return std::make_pair(rowAsNumber(rowPart), columnAsNumber(columnPart)); + /* 2024-06-19 OBSOLETE CODE + // auto it = std::find_if(address.begin(), address.end(), ::isdigit); + // auto columnPart = std::string(address.begin(), it); + // auto rowPart = std::string(it, address.end()); + // + // return std::make_pair(rowAsNumber(rowPart), columnAsNumber(columnPart)); + */ } diff --git a/OpenXLSX/sources/XLCellValue.cpp b/OpenXLSX/sources/XLCellValue.cpp index 85d779a..2a7f42a 100644 --- a/OpenXLSX/sources/XLCellValue.cpp +++ b/OpenXLSX/sources/XLCellValue.cpp @@ -121,9 +121,9 @@ std::string XLCellValue::typeAsString() const */ XLCellValueProxy::XLCellValueProxy(XLCell* cell, XMLNode* cellNode) : m_cell(cell), m_cellNode(cellNode) { - assert(cell); // NOLINT -// assert(cellNode); // NOLINT -// assert(!cellNode->empty()); // NOLINT + assert(cell != nullptr); // NOLINT +// assert(cellNode); // NOLINT +// assert(not cellNode->empty()); // NOLINT } /** @@ -187,8 +187,8 @@ XLCellValueProxy::operator XLCellValue() const { XLCellValueProxy& XLCellValueProxy::clear() { // ===== Check that the m_cellNode is valid. - assert(m_cellNode); // NOLINT - assert(!m_cellNode->empty()); // NOLINT + assert(m_cellNode != nullptr); // NOLINT + assert(not m_cellNode->empty()); // NOLINT // ===== Remove the type attribute m_cellNode->remove_attribute("t"); @@ -213,8 +213,8 @@ XLCellValueProxy& XLCellValueProxy::clear() XLCellValueProxy& XLCellValueProxy::setError(const std::string &error) { // ===== Check that the m_cellNode is valid. - assert(m_cellNode); // NOLINT - assert(!m_cellNode->empty()); // NOLINT + assert(m_cellNode != nullptr); // NOLINT + assert(not m_cellNode->empty()); // NOLINT // ===== If the cell node doesn't have a type attribute, create it. if (!m_cellNode->attribute("t")) m_cellNode->append_attribute("t"); @@ -245,34 +245,34 @@ XLCellValueProxy& XLCellValueProxy::setError(const std::string &error) XLValueType XLCellValueProxy::type() const { // ===== Check that the m_cellNode is valid. - assert(m_cellNode); // NOLINT - assert(!m_cellNode->empty()); // NOLINT + assert(m_cellNode != nullptr); // NOLINT + assert(not m_cellNode->empty()); // NOLINT // ===== If neither a Type attribute or a getValue node is present, the cell is empty. if (!m_cellNode->attribute("t") && !m_cellNode->child("v")) return XLValueType::Empty; // ===== If a Type attribute is not present, but a value node is, the cell contains a number. - if (!m_cellNode->attribute("t") || ((strcmp(m_cellNode->attribute("t").value(), "n") == 0) && !m_cellNode->child("v").empty())) { - if (const std::string numberString = m_cellNode->child("v").text().get(); numberString.find('.') != std::string::npos || numberString.find("E-") != std::string::npos || - numberString.find("e-") != std::string::npos) + if (m_cellNode->attribute("t").empty() || ((strcmp(m_cellNode->attribute("t").value(), "n") == 0) && not m_cellNode->child("v").empty())) { + if (const std::string numberString = m_cellNode->child("v").text().get(); + numberString.find('.') != std::string::npos || numberString.find("E-") != std::string::npos || numberString.find("e-") != std::string::npos) return XLValueType::Float; return XLValueType::Integer; } // ===== If the cell is of type "s", the cell contains a shared string. - if (!m_cellNode->attribute("t").empty() && strcmp(m_cellNode->attribute("t").value(), "s") == 0) + if (not m_cellNode->attribute("t").empty() && strcmp(m_cellNode->attribute("t").value(), "s") == 0) return XLValueType::String; // NOLINT // ===== If the cell is of type "inlineStr", the cell contains an inline string. - if (!m_cellNode->attribute("t").empty() && strcmp(m_cellNode->attribute("t").value(), "inlineStr") == 0) + if (not m_cellNode->attribute("t").empty() && strcmp(m_cellNode->attribute("t").value(), "inlineStr") == 0) return XLValueType::String; // ===== If the cell is of type "str", the cell contains an ordinary string. - if (!m_cellNode->attribute("t").empty() && strcmp(m_cellNode->attribute("t").value(), "str") == 0) + if (not m_cellNode->attribute("t").empty() && strcmp(m_cellNode->attribute("t").value(), "str") == 0) return XLValueType::String; // ===== If the cell is of type "b", the cell contains a boolean. - if (!m_cellNode->attribute("t").empty() && strcmp(m_cellNode->attribute("t").value(), "b") == 0) + if (not m_cellNode->attribute("t").empty() && strcmp(m_cellNode->attribute("t").value(), "b") == 0) return XLValueType::Boolean; // ===== Otherwise, the cell contains an error. @@ -311,11 +311,11 @@ std::string XLCellValueProxy::typeAsString() const void XLCellValueProxy::setInteger(int64_t numberValue) // NOLINT { // ===== Check that the m_cellNode is valid. - assert(m_cellNode); // NOLINT - assert(!m_cellNode->empty()); // NOLINT + assert(m_cellNode != nullptr); // NOLINT + assert(not m_cellNode->empty()); // NOLINT // ===== If the cell node doesn't have a value child node, create it. - if (!m_cellNode->child("v")) m_cellNode->append_child("v"); + if (m_cellNode->child("v").empty()) m_cellNode->append_child("v"); // ===== The type ("t") attribute is not required for number values. m_cellNode->remove_attribute("t"); @@ -339,14 +339,14 @@ void XLCellValueProxy::setInteger(int64_t numberValue) // NOLINT void XLCellValueProxy::setBoolean(bool numberValue) // NOLINT { // ===== Check that the m_cellNode is valid. - assert(m_cellNode); // NOLINT - assert(!m_cellNode->empty()); // NOLINT + assert(m_cellNode != nullptr); // NOLINT + assert(not m_cellNode->empty()); // NOLINT // ===== If the cell node doesn't have a type child node, create it. - if (!m_cellNode->attribute("t")) m_cellNode->append_attribute("t"); + if (m_cellNode->attribute("t").empty()) m_cellNode->append_attribute("t"); // ===== If the cell node doesn't have a value child node, create it. - if (!m_cellNode->child("v")) m_cellNode->append_child("v"); + if (m_cellNode->child("v").empty()) m_cellNode->append_child("v"); // ===== Set the type attribute. m_cellNode->attribute("t").set_value("b"); @@ -372,11 +372,11 @@ void XLCellValueProxy::setFloat(double numberValue) // check for nan / inf if (std::isfinite(numberValue)) { // ===== Check that the m_cellNode is valid. - assert(m_cellNode); // NOLINT - assert(!m_cellNode->empty()); // NOLINT + assert(m_cellNode != nullptr); // NOLINT + assert(not m_cellNode->empty()); // NOLINT // ===== If the cell node doesn't have a value child node, create it. - if (!m_cellNode->child("v")) m_cellNode->append_child("v"); + if (m_cellNode->child("v").empty()) m_cellNode->append_child("v"); // ===== The type ("t") attribute is not required for number values. m_cellNode->remove_attribute("t"); @@ -405,14 +405,14 @@ void XLCellValueProxy::setFloat(double numberValue) void XLCellValueProxy::setString(const char* stringValue) // NOLINT { // ===== Check that the m_cellNode is valid. - assert(m_cellNode); // NOLINT - assert(!m_cellNode->empty()); // NOLINT + assert(m_cellNode != nullptr); // NOLINT + assert(not m_cellNode->empty()); // NOLINT // ===== If the cell node doesn't have a type child node, create it. - if (!m_cellNode->attribute("t")) m_cellNode->append_attribute("t"); + if (m_cellNode->attribute("t").empty()) m_cellNode->append_attribute("t"); // ===== If the cell node doesn't have a value child node, create it. - if (!m_cellNode->child("v")) m_cellNode->append_child("v"); + if (m_cellNode->child("v").empty()) m_cellNode->append_child("v"); // ===== Set the type attribute. m_cellNode->attribute("t").set_value("s"); @@ -451,8 +451,8 @@ void XLCellValueProxy::setString(const char* stringValue) // NOLINT XLCellValue XLCellValueProxy::getValue() const { // ===== Check that the m_cellNode is valid. - assert(m_cellNode); // NOLINT - assert(!m_cellNode->empty()); // NOLINT + assert(m_cellNode != nullptr); // NOLINT + assert(not m_cellNode->empty()); // NOLINT switch (type()) { case XLValueType::Empty: diff --git a/OpenXLSX/sources/XLColor.cpp b/OpenXLSX/sources/XLColor.cpp index 716bee3..8fd49aa 100644 --- a/OpenXLSX/sources/XLColor.cpp +++ b/OpenXLSX/sources/XLColor.cpp @@ -45,6 +45,7 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. // ===== External Includes ===== // #include // pull requests #216, #232 +#include // std::hex #include // ===== OpenXLSX Includes ===== // diff --git a/OpenXLSX/sources/XLColumn.cpp b/OpenXLSX/sources/XLColumn.cpp index bda12ed..02d1a50 100644 --- a/OpenXLSX/sources/XLColumn.cpp +++ b/OpenXLSX/sources/XLColumn.cpp @@ -80,13 +80,13 @@ void XLColumn::setWidth(float width) // NOLINT { // Set the 'Width' attribute for the Cell. If it does not exist, create it. auto widthAtt = columnNode().attribute("width"); - if (!widthAtt) widthAtt = columnNode().append_attribute("width"); + if (widthAtt.empty()) widthAtt = columnNode().append_attribute("width"); widthAtt.set_value(width); // Set the 'customWidth' attribute for the Cell. If it does not exist, create it. auto customAtt = columnNode().attribute("customWidth"); - if (!customAtt) customAtt = columnNode().append_attribute("customWidth"); + if (customAtt.empty()) customAtt = columnNode().append_attribute("customWidth"); customAtt.set_value("1"); } @@ -102,7 +102,7 @@ bool XLColumn::isHidden() const { return columnNode().attribute("hidden").as_boo void XLColumn::setHidden(bool state) // NOLINT { auto hiddenAtt = columnNode().attribute("hidden"); - if (!hiddenAtt) hiddenAtt = columnNode().append_attribute("hidden"); + if (hiddenAtt.empty()) hiddenAtt = columnNode().append_attribute("hidden"); if (state) hiddenAtt.set_value("1"); diff --git a/OpenXLSX/sources/XLContentTypes.cpp b/OpenXLSX/sources/XLContentTypes.cpp index 93df3f7..d505b84 100644 --- a/OpenXLSX/sources/XLContentTypes.cpp +++ b/OpenXLSX/sources/XLContentTypes.cpp @@ -289,7 +289,7 @@ std::vector XLContentTypes::getContentItems() { std::vector result; XMLNode item = xmlDocument().document_element().first_child_of_type(pugi::node_element); - while (item) { + while (not item.empty()) { if (strcmp(item.name(), "Override") == 0) result.emplace_back(item); item = item.next_sibling_of_type(pugi::node_element); } diff --git a/OpenXLSX/sources/XLDateTime.cpp b/OpenXLSX/sources/XLDateTime.cpp index 8dce794..6714771 100644 --- a/OpenXLSX/sources/XLDateTime.cpp +++ b/OpenXLSX/sources/XLDateTime.cpp @@ -5,8 +5,7 @@ #include "XLDateTime.hpp" #include "XLException.hpp" #include -#include -#include +#include // int32_t namespace { diff --git a/OpenXLSX/sources/XLDocument.cpp b/OpenXLSX/sources/XLDocument.cpp index 23559e7..dc72e2a 100644 --- a/OpenXLSX/sources/XLDocument.cpp +++ b/OpenXLSX/sources/XLDocument.cpp @@ -44,6 +44,7 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. */ // ===== External Includes ===== // +#include #ifdef ENABLE_NOWIDE # include #endif @@ -58,8 +59,6 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. #include "XLSheet.hpp" #include "utilities/XLUtilities.hpp" -#include - using namespace OpenXLSX; namespace @@ -431,7 +430,10 @@ XLDocument::XLDocument(const std::string& docPath, const IZipArchive& zipArchive /** * @details The destructor calls the closeDocument method before the object is destroyed. */ -XLDocument::~XLDocument() { close(); } +XLDocument::~XLDocument() +{ + if (isOpen()) close();// 2024-05-31 prevent double-close if document has been manually closed before +} /** * @details The openDocument method opens the .xlsx package in the following manner: @@ -443,8 +445,7 @@ XLDocument::~XLDocument() { close(); } void XLDocument::open(const std::string& fileName) { // Check if a document is already open. If yes, close it. - // TODO: Consider throwing if a file is already open. - if (m_archive.isOpen()) close(); + if (m_archive.isOpen()) close(); // TBD: consider throwing if a file is already open. m_filePath = fileName; m_archive.open(m_filePath); @@ -476,56 +477,64 @@ void XLDocument::open(const std::string& fileName) // ===== Read shared strings table. XMLDocument* sharedStrings = getXmlData("xl/sharedStrings.xml")->getXmlDocument(); - if (!sharedStrings->document_element().attribute("uniqueCount").empty()) + if (not sharedStrings->document_element().attribute("uniqueCount").empty()) sharedStrings->document_element().remove_attribute( "uniqueCount"); // pull request #192 -> remove count & uniqueCount as they are optional - if (!sharedStrings->document_element().attribute("count").empty()) + if (not sharedStrings->document_element().attribute("count").empty()) sharedStrings->document_element().remove_attribute( - "count"); // pull request #192 -> remove count & uniqueCount as they are optional + "count"); // pull request #192 -> remove count & uniqueCount as they are optional XMLNode node = sharedStrings->document_element().first_child_of_type(pugi::node_element); // pull request #186: Skip non-element nodes in sst. - while (node) { + while (not node.empty()) { // ===== Validate si node name. using namespace std::literals::string_literals; if (node.name() != "si"s) throw XLInputError("xl/sharedStrings.xml sst node name \""s + node.name() + "\" is not \"si\""s); // ===== Find first node_element child of si node. - if (XMLNode elem = node.first_child_of_type(pugi::node_element)) { - // ===== If shared string is a rich text string + XMLNode elem = node.first_child_of_type(pugi::node_element); + if (not elem.empty()) { + // ===== Ignore phonetic property tags if (std::string(elem.name()) == "rPh" || std::string(elem.name()) == "phoneticPr") {} + // ===== If shared string is a rich text string else if (std::string(elem.name()) == "r") { std::string result; - while (elem) { + while (not elem.empty()) { result += elem.child("t").text().get(); elem = elem.next_sibling_of_type(pugi::node_element); } m_sharedStringCache.emplace_back(result); } // ===== If shared string is a regular string - else if (std::string(elem.name()) == "t") { // 2024-05-03: support a string composed of multiple nodes, because LibreOffice accepts it + else { // 2024-05-03: support a string composed of multiple nodes, because LibreOffice accepts it std::string result; - while (elem) { - // if (elem.name() != "t"s) - // throw XLInputError("xl/sharedStrings.xml si node \""s + node.name() + "\" is neiter \"r\" not \"t\""s); + while (not elem.empty()) { + if (elem.name() != "t"s) + throw XLInputError("xl/sharedStrings.xml si node \""s + node.name() + "\" is none of \"r\", \"t\", \"rPh\", \"phoneticPr\""s); result += elem.text().get(); elem = elem.next_sibling_of_type(pugi::node_element); } m_sharedStringCache.emplace_back(result); } - else - throw XLInputError("xl/sharedStrings.xml si node \""s + node.name() + "\" is neiter \"r\" not \"t\""s); - } node = node.next_sibling_of_type(pugi::node_element); } // ===== Open the workbook and document property items - // TODO: If property data doesn't exist, consider creating them, instead of ignoring it. - m_coreProperties = (hasXmlData("docProps/core.xml") ? XLProperties(getXmlData("docProps/core.xml")) : XLProperties()); - m_appProperties = (hasXmlData("docProps/app.xml") ? XLAppProperties(getXmlData("docProps/app.xml")) : XLAppProperties()); - m_sharedStrings = XLSharedStrings(getXmlData("xl/sharedStrings.xml"), &m_sharedStringCache); m_workbook = XLWorkbook(getXmlData("xl/workbook.xml")); + // 2024-05-31: moved XLWorkbook object creation up in code worksheets info can be used for XLAppProperties generation from scratch + + // ===== 2024-06-03: creating core and extended properties if they do not exist + execCommand(XLCommand(XLCommandType::CheckAndFixCoreProperties)); // checks & fixes consistency of docProps/core.xml related data + execCommand(XLCommand(XLCommandType::CheckAndFixExtendedProperties)); // checks & fixes consistency of docProps/app.xml related data + + if (!hasXmlData("docProps/core.xml") || !hasXmlData("docProps/app.xml")) + throw XLInternalError("Failed to repair docProps (core.xml and/or app.xml)"); + + m_coreProperties = XLProperties(getXmlData("docProps/core.xml")); + m_appProperties = XLAppProperties(getXmlData("docProps/app.xml"), m_workbook.xmlDocument()); + + m_sharedStrings = XLSharedStrings(getXmlData("xl/sharedStrings.xml"), &m_sharedStringCache); } /** @@ -553,7 +562,7 @@ void XLDocument::create(const std::string& fileName) */ void XLDocument::close() { - if (m_archive) m_archive.close(); + if (m_archive.isValid()) m_archive.close(); m_filePath.clear(); m_data.clear(); @@ -590,9 +599,15 @@ void XLDocument::saveAs(const std::string& fileName) /** * @details - * @todo Currently, this method returns the full path, which is not the intention. */ -const std::string& XLDocument::name() const { return m_filePath; } +const std::string XLDocument::name() const +{ + size_t pos = m_filePath.find_last_of('/'); + if (pos != std::string::npos) + return m_filePath.substr(pos + 1); + else + return m_filePath; +} /** * @details @@ -679,7 +694,6 @@ bool getAppVersion(const std::string& versionString, int& majorVersion, int& min const size_t end = versionString.find_last_not_of(" \t"); if (begin != std::string::npos && dotPos != std::string::npos) { - std::string trimmedValue = versionString.substr(begin, end + 1 - begin); const std::string strMajorVersion = versionString.substr(begin, dotPos - begin); const std::string strMinorVersion = versionString.substr(dotPos + 1, end - dotPos); try { @@ -844,7 +858,7 @@ bool XLDocument::execCommand(const XLCommand& command) case XLCommandType::SetSheetIndex: { XLQuery qry(XLQueryType::QuerySheetName); - const auto sheetName = execQuery(qry.setParam("sheetID", command.getParam("sheetID"))).result(); + const auto sheetName = execQuery(qry.setParam("sheetID", command.getParam("sheetID"))).result(); m_workbook.setSheetIndex(sheetName, command.getParam("sheetIndex")); } break; @@ -859,6 +873,46 @@ bool XLDocument::execCommand(const XLCommand& command) if (item != m_data.end()) m_data.erase(item); } break; + case XLCommandType::CheckAndFixCoreProperties: { // does nothing if core properties are in good shape + // ===== If _rels/.rels has no entry for docProps/core.xml + if (!m_docRelationships.targetExists("docProps/core.xml")) + m_docRelationships.addRelationship(XLRelationshipType::CoreProperties, "docProps/core.xml"); // Fix m_docRelationships + + // ===== If docProps/core.xml is missing + if (!m_archive.hasEntry("docProps/core.xml")) + m_archive.addEntry("docProps/core.xml", ""); // create empty docProps/core.xml + // ===== XLProperties constructor will take care of adding template content + + // ===== If [Content Types].xml has no relationship for docProps/core.xml + if (!hasXmlData("docProps/core.xml")) { + m_contentTypes.addOverride("/docProps/core.xml", XLContentType::CoreProperties); // add content types entry + m_data.emplace_back( // store new entry in m_data + /* parentDoc */ this, + /* xmlPath */ "docProps/core.xml", + /* xmlID */ m_docRelationships.relationshipByTarget("docProps/core.xml").id(), + /* xmlType */ XLContentType::CoreProperties); + } + } break; + case XLCommandType::CheckAndFixExtendedProperties: { // does nothing if extended properties are in good shape + // ===== If _rels/.rels has no entry for docProps/app.xml + if (!m_docRelationships.targetExists("docProps/app.xml")) + m_docRelationships.addRelationship(XLRelationshipType::ExtendedProperties, "docProps/app.xml"); // Fix m_docRelationships + + // ===== If docProps/app.xml is missing + if (!m_archive.hasEntry("docProps/app.xml")) + m_archive.addEntry("docProps/app.xml", ""); // create empty docProps/app.xml + // ===== XLAppProperties constructor will take care of adding template content + + // ===== If [Content Types].xml has no relationship for docProps/app.xml + if (!hasXmlData("docProps/app.xml")) { + m_contentTypes.addOverride("/docProps/app.xml", XLContentType::ExtendedProperties); // add content types entry + m_data.emplace_back( // store new entry in m_data + /* parentDoc */ this, + /* xmlPath */ "docProps/app.xml", + /* xmlID */ m_docRelationships.relationshipByTarget("docProps/app.xml").id(), + /* xmlType */ XLContentType::ExtendedProperties); + } + } break; case XLCommandType::AddSharedStrings: { const std::string sharedStrings { "\n" @@ -1004,6 +1058,8 @@ XLQuery XLDocument::execQuery(const XLQuery& query) const throw XLInternalError("Path does not exist in zip archive (" + query.getParam("xmlPath") + ")"); return XLQuery(query).setResult(&*result); } + default: + throw XLInternalError("XLDocument::execQuery: unknown query type " + std::to_string(static_cast(query.type()))); } return query; // Needed in order to suppress compiler warning @@ -1019,7 +1075,7 @@ XLQuery XLDocument::execQuery(const XLQuery& query) { return static_cast #include +// ===== OpenXLSX Includes ===== // +#include "XLFormula.hpp" #include -#include using namespace OpenXLSX; @@ -121,11 +122,11 @@ std::string XLFormulaProxy::get() const { return getFormula().get(); } XLFormulaProxy& XLFormulaProxy::clear() { // ===== Check that the m_cellNode is valid. - assert(m_cellNode); // NOLINT - assert(!m_cellNode->empty()); // NOLINT + assert(m_cellNode != nullptr); // NOLINT + assert(not m_cellNode->empty()); // NOLINT // ===== Remove the value node. - if (m_cellNode->child("f")) m_cellNode->remove_child("f"); + if (not m_cellNode->child("f").empty()) m_cellNode->remove_child("f"); return *this; } @@ -136,12 +137,12 @@ XLFormulaProxy& XLFormulaProxy::clear() void XLFormulaProxy::setFormulaString(const char* formulaString, bool resetValue) // NOLINT { // ===== Check that the m_cellNode is valid. - assert(m_cellNode); // NOLINT - assert(!m_cellNode->empty()); // NOLINT + assert(m_cellNode != nullptr); // NOLINT + assert(not m_cellNode->empty()); // NOLINT // ===== If the cell node doesn't have a value child node, create it. - if (!m_cellNode->child("f")) m_cellNode->append_child("f"); - if (!m_cellNode->child("v")) m_cellNode->append_child("v"); + if (m_cellNode->child("f").empty()) m_cellNode->append_child("f"); + if (m_cellNode->child("v").empty()) m_cellNode->append_child("v"); // ===== Remove the formula type and shared index attributes, if they exist. m_cellNode->child("f").remove_attribute("t"); @@ -169,19 +170,22 @@ void XLFormulaProxy::setFormulaString(const char* formulaString, bool resetValue */ XLFormula XLFormulaProxy::getFormula() const { - assert(m_cellNode); // NOLINT - assert(!m_cellNode->empty()); // NOLINT + assert(m_cellNode != nullptr); // NOLINT + assert(not m_cellNode->empty()); // NOLINT const auto formulaNode = m_cellNode->child("f"); // ===== If the formula node doesn't exist, return an empty XLFormula object. - if (!formulaNode) return XLFormula(); + if (formulaNode.empty()) return XLFormula(); // ===== If the formula type is 'shared' or 'array', throw an exception. - if (formulaNode.attribute("t") && std::string(formulaNode.attribute("t").value()) == "shared") - throw XLFormulaError("Shared formulas not supported."); - if (formulaNode.attribute("t") && std::string(formulaNode.attribute("t").value()) == "array") - throw XLFormulaError("Array formulas not supported."); + if (not formulaNode.attribute("t").empty() ) { // 2024-05-28: de-duplicated check (only relevant for performance, + // xml_attribute::value() returns an empty string for empty attributes) + if (std::string(formulaNode.attribute("t").value()) == "shared") + throw XLFormulaError("Shared formulas not supported."); + if (std::string(formulaNode.attribute("t").value()) == "array") + throw XLFormulaError("Array formulas not supported."); + } return XLFormula(formulaNode.text().get()); } diff --git a/OpenXLSX/sources/XLProperties.cpp b/OpenXLSX/sources/XLProperties.cpp index 0469b36..3abfa0a 100644 --- a/OpenXLSX/sources/XLProperties.cpp +++ b/OpenXLSX/sources/XLProperties.cpp @@ -51,6 +51,7 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. // ===== OpenXLSX Includes ===== // #include "XLDocument.hpp" #include "XLProperties.hpp" +#include "XLRelationships.hpp" // GetStringFromType #include @@ -67,15 +68,16 @@ namespace std::vector headingPairsCategoriesStrings(XMLNode docNode) { - // TODO: test this again + // 2024-05-28 DONE: tested this code with two pairs in headingPairsNode std::vector result; XMLNode item = headingPairsNode(docNode).first_child_of_type(pugi::node_element); - while (item) { + while (not item.empty()) { result.push_back(item.first_child_of_type(pugi::node_element).child_value()); item = item.next_sibling_of_type(pugi::node_element) .next_sibling_of_type(pugi::node_element); // advance two elements to skip count node } - return std::move(result); + return result; // 2024-05-28: std::move should not be used when the operand of a return statement is the name of a local variable + // as this can prevent named return value optimization (NRVO, copy elision) } XMLNode sheetNames(XMLNode docNode) { return docNode.child("TitlesOfParts").first_child_of_type(pugi::node_element); } @@ -86,7 +88,58 @@ namespace /** * @details */ -XLProperties::XLProperties(XLXmlData* xmlData) : XLXmlFile(xmlData) {} +void XLProperties::createFromTemplate() +{ + // std::cout << "XLProperties created with empty docProps/core.xml, creating from scratch!" << std::endl; + if( m_xmlData == nullptr ) + throw XLInternalError("XLProperties m_xmlData is nullptr"); + + // NOTE: there is no functionality in pugixml to include the standalone="yes" attribute in the element node + + // ===== OpenXLSX_XLRelationships::GetStringFromType yields almost the string needed here, with added /relationships + // TBD: use hardcoded string? + std::string xmlns = OpenXLSX_XLRelationships::GetStringFromType(XLRelationshipType::CoreProperties); + const std::string rels = "/relationships/"; + size_t pos = xmlns.find(rels); + if (pos != std::string::npos) + xmlns.replace(pos, rels.size(), "/"); + else + xmlns = "http://schemas.openxmlformats.org/package/2006/metadata/core-properties"; // fallback to hardcoded string + + XMLNode props = xmlDocument().prepend_child("cp:coreProperties"); + props.append_attribute("xmlns:cp") = xmlns.c_str(); + props.append_attribute("xmlns:dc") = "http://purl.org/dc/elements/1.1/"; + props.append_attribute("xmlns:dcterms") = "http://purl.org/dc/terms/"; + props.append_attribute("xmlns:dcmitype") = "http://purl.org/dc/dcmitype/"; + props.append_attribute("xmlns:xsi") = "http://www.w3.org/2001/XMLSchema-instance"; + + props.append_child("dc:creator").text().set("Kenneth Balslev"); + props.append_child("cp:lastModifiedBy").text().set("Kenneth Balslev"); + + XMLNode prop {}; + prop = props.append_child("dcterms:created"); + prop.append_attribute("xsi:type") = "dcterms:W3CDTF"; + prop.text().set("2019-08-16T00:34:14Z"); + prop = props.append_child("dcterms:modified"); + prop.append_attribute("xsi:type") = "dcterms:W3CDTF"; + prop.text().set("2019-08-16T00:34:26Z"); +} + +/** + * @details + */ +XLProperties::XLProperties(XLXmlData* xmlData) : XLXmlFile(xmlData) +{ + XMLNode doc = xmlData->getXmlDocument()->document_element(); + XMLNode child = doc.first_child_of_type(pugi::node_element); + size_t childCount = 0; + while (not child.empty()) { + ++childCount; + child = child.next_sibling_of_type(pugi::node_element); + break; // one child is enough to determine document is not empty. + } + if( !childCount ) createFromTemplate(); +} /** * @details @@ -98,11 +151,10 @@ XLProperties::~XLProperties() = default; */ void XLProperties::setProperty(const std::string& name, const std::string& value) { - if (!m_xmlData) return; + if (m_xmlData == nullptr) return; XMLNode node = xmlDocument().document_element().child(name.c_str()); if (node.empty()) - node = xmlDocument().document_element().append_child( - name.c_str()); // changed this to append, to be in line with ::property behavior, TBD whether this should stay prepend + node = xmlDocument().document_element().append_child(name.c_str()); // .append_child, to be in line with ::property behavior node.text().set(value.c_str()); } @@ -122,7 +174,7 @@ void XLProperties::setProperty(const std::string& name, double value) { setPrope */ std::string XLProperties::property(const std::string& name) const { - if (!m_xmlData) return ""; + if (m_xmlData == nullptr) return ""; XMLNode property = xmlDocument().document_element().child(name.c_str()); if (property.empty()) property = xmlDocument().document_element().append_child(name.c_str()); @@ -134,11 +186,96 @@ std::string XLProperties::property(const std::string& name) const */ void XLProperties::deleteProperty(const std::string& name) { - if (!m_xmlData) return; + if (m_xmlData == nullptr) return; if (const XMLNode property = xmlDocument().document_element().child(name.c_str()); not property.empty()) xmlDocument().document_element().remove_child(property); } + +/** + * @details + */ +void XLAppProperties::createFromTemplate(XMLDocument const & workbookXml) +{ + // std::cout << "XLAppProperties created with empty docProps/app.xml, creating from scratch!" << std::endl; + if( m_xmlData == nullptr ) + throw XLInternalError("XLAppProperties m_xmlData is nullptr"); + + std::map< uint32_t, std::string > sheetsOrderedById; + + auto sheet = workbookXml.document_element().child("sheets").first_child_of_type(pugi::node_element); + while (not sheet.empty()) { + std::string sheetName = sheet.attribute("name").as_string(); + uint32_t sheetId = sheet.attribute("sheetId").as_uint(); + sheetsOrderedById.insert(std::pair(sheetId, sheetName)); + sheet = sheet.next_sibling_of_type(); + } + + uint32_t worksheetCount = 0; + for (const auto & [key, value] : sheetsOrderedById) { + if (key != ++worksheetCount) + throw XLInputError( "xl/workbook.xml is missing sheet with sheetId=\"" + std::to_string(worksheetCount) + "\"" ); + } + + // NOTE: there is no functionality in pugixml to include the standalone="yes" attribute in the element node + + // ===== OpenXLSX_XLRelationships::GetStringFromType yields almost the string needed here, with added /relationships + // TBD: use hardcoded string? + std::string xmlns = OpenXLSX_XLRelationships::GetStringFromType(XLRelationshipType::ExtendedProperties); + const std::string rels = "/relationships/"; + size_t pos = xmlns.find(rels); + if (pos != std::string::npos) + xmlns.replace(pos, rels.size(), "/"); + else + xmlns = "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"; // fallback to hardcoded string + + XMLNode props = xmlDocument().prepend_child("Properties"); + props.append_attribute("xmlns") = xmlns.c_str(); + props.append_attribute("xmlns:vt") = "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"; + + XMLNode prop {}; + + props.append_child("Application").text().set("Microsoft Macintosh Excel"); + props.append_child("DocSecurity").text().set(0); + props.append_child("ScaleCrop").text().set(false); + + XMLNode headingPairs = props.append_child("HeadingPairs"); + XMLNode vecHP = headingPairs.append_child("vt:vector"); + vecHP.append_attribute("size") = 2; + vecHP.append_attribute("baseType") = "variant"; + vecHP.append_child("vt:variant").append_child("vt:lpstr").text().set("Worksheets"); + vecHP.append_child("vt:variant").append_child("vt:i4").text().set(1); // TBD: should this be count of worksheets? + + XMLNode sheetsVector = headingPairs.append_child("TitlesOfParts").append_child("vt:vector"); + sheetsVector.append_attribute("size") = worksheetCount; + sheetsVector.append_attribute("baseType") = "lpstr"; + for (const auto & [key, value] : sheetsOrderedById) + sheetsVector.append_child("vt:lpstr").text().set(value.c_str()); + + props.append_child("Company").text().set(""); + props.append_child("LinksUpToDate").text().set(false); + props.append_child("SharedDoc").text().set(false); + props.append_child("HyperlinksChanged").text().set(false); + props.append_child("AppVersion").text().set("16.0300"); +} + +/** + * @details + */ +XLAppProperties::XLAppProperties(XLXmlData* xmlData, XMLDocument const & workbookXml) + : XLXmlFile(xmlData) +{ + XMLNode doc = xmlData->getXmlDocument()->document_element(); + XMLNode child = doc.first_child_of_type(pugi::node_element); + size_t childCount = 0; + while (not child.empty()) { + ++childCount; + child = child.next_sibling_of_type(pugi::node_element); + break; // one child is enough to determine document is not empty. + } + if (!childCount) createFromTemplate(workbookXml); // create fresh docProps/app.xml +} + /** * @details */ @@ -154,8 +291,8 @@ XLAppProperties::~XLAppProperties() = default; */ void XLAppProperties::addSheetName(const std::string& title) { - if (!m_xmlData) return; - const XMLNode theNode = sheetNames(xmlDocument().document_element()).append_child("vt:lpstr"); + if (m_xmlData == nullptr) return; + XMLNode theNode = sheetNames(xmlDocument().document_element()).append_child("vt:lpstr"); theNode.text().set(title.c_str()); sheetCount(xmlDocument().document_element()).set_value(sheetCount(xmlDocument().document_element()).as_uint() + 1); } @@ -165,15 +302,15 @@ void XLAppProperties::addSheetName(const std::string& title) */ void XLAppProperties::deleteSheetName(const std::string& title) { - if (!m_xmlData) return; - XMLNode iter = sheetNames(xmlDocument().document_element()).first_child_of_type(pugi::node_element); - while (iter) { - if (iter.child_value() == title) { - sheetNames(xmlDocument().document_element()).remove_child(iter); + if (m_xmlData == nullptr) return; + XMLNode theNode = sheetNames(xmlDocument().document_element()).first_child_of_type(pugi::node_element); + while (not theNode.empty()) { + if (theNode.child_value() == title) { + sheetNames(xmlDocument().document_element()).remove_child(theNode); sheetCount(xmlDocument().document_element()).set_value(sheetCount(xmlDocument().document_element()).as_uint() - 1); return; } - iter = iter.next_sibling_of_type(pugi::node_element); + theNode = theNode.next_sibling_of_type(pugi::node_element); } } @@ -182,14 +319,14 @@ void XLAppProperties::deleteSheetName(const std::string& title) */ void XLAppProperties::setSheetName(const std::string& oldTitle, const std::string& newTitle) { - if (!m_xmlData) return; - XMLNode iter = sheetNames(xmlDocument().document_element()).first_child_of_type(pugi::node_element); - while (iter) { - if (iter.child_value() == oldTitle) { - iter.text().set(newTitle.c_str()); + if (m_xmlData == nullptr) return; + XMLNode theNode = sheetNames(xmlDocument().document_element()).first_child_of_type(pugi::node_element); + while (not theNode.empty()) { + if (theNode.child_value() == oldTitle) { + theNode.text().set(newTitle.c_str()); return; } - iter = iter.next_sibling_of_type(pugi::node_element); + theNode = theNode.next_sibling_of_type(pugi::node_element); } } @@ -198,31 +335,31 @@ void XLAppProperties::setSheetName(const std::string& oldTitle, const std::strin */ void XLAppProperties::addHeadingPair(const std::string& name, int value) { - if (!m_xmlData) return; + if (m_xmlData == nullptr) return; XMLNode HeadingPairsNode = headingPairsNode(xmlDocument().document_element()); XMLNode item = HeadingPairsNode.first_child_of_type(pugi::node_element); - while (!item.empty() && item.first_child_of_type(pugi::node_element).child_value() != name) + while (not item.empty() && item.first_child_of_type(pugi::node_element).child_value() != name) item = item.next_sibling_of_type(pugi::node_element) .next_sibling_of_type(pugi::node_element); // advance two elements to skip count node XMLNode pairCategory = item; // could be an empty node XMLNode pairCountValue {}; // initialize to empty node - if (!pairCategory.empty()) + if (not pairCategory.empty()) pairCountValue = pairCategory.next_sibling_of_type(pugi::node_element).first_child_of_type(pugi::node_element); else { item = HeadingPairsNode.last_child_of_type(pugi::node_element); - if (item) + if (not item.empty()) pairCategory = HeadingPairsNode.insert_child_after("vt:variant", item); else pairCategory = HeadingPairsNode.append_child("vt:variant"); - const XMLNode categoryName = pairCategory.append_child("vt:lpstr"); + XMLNode categoryName = pairCategory.append_child("vt:lpstr"); categoryName.text().set(name.c_str()); XMLNode pairCount = HeadingPairsNode.insert_child_after("vt:variant", pairCategory); pairCountValue = pairCount.append_child("vt:i4"); } - if (pairCountValue) + if (not pairCountValue.empty()) pairCountValue.text().set(std::to_string(value).c_str()); else { using namespace std::literals::string_literals; @@ -236,25 +373,30 @@ void XLAppProperties::addHeadingPair(const std::string& name, int value) */ void XLAppProperties::deleteHeadingPair(const std::string& name) { - if (!m_xmlData) return; + if (m_xmlData == nullptr) return; XMLNode HeadingPairsNode = headingPairsNode(xmlDocument().document_element()); XMLNode item = HeadingPairsNode.first_child_of_type(pugi::node_element); - while (item && item.first_child_of_type(pugi::node_element).child_value() != name) + while (not item.empty() && item.first_child_of_type(pugi::node_element).child_value() != name) item = item.next_sibling_of_type(pugi::node_element) .next_sibling_of_type(pugi::node_element); // advance two elements to skip count node // ===== If item with name was found, remove pair and update headingPairsSize - if (!item.empty()) { - if (const XMLNode count = item.next_sibling_of_type(pugi::node_element)) { - if (!count.empty()) { // 2024-05-02: TBD that change to loop deleting whitespaces following a count node is bug-free - while (item.next_sibling() != count) - HeadingPairsNode.remove_child(item.next_sibling()); // remove all nodes between element nodes to be deleted jointly - HeadingPairsNode.remove_child(count); - } + if (not item.empty()) { + const XMLNode count = item.next_sibling_of_type(pugi::node_element); + // ===== 2024-05-28: delete all (non-element) nodes between item and count node, *then* delete non-element nodes following a count node + if (not count.empty()) { + while (item.next_sibling() != count) + HeadingPairsNode.remove_child(item.next_sibling()); // remove nodes between item & count nodes to be deleted jointly + + // ===== Delete all non-element nodes following the count node + while ((not count.next_sibling().empty()) && (count.next_sibling().type() != pugi::node_element)) + HeadingPairsNode.remove_child(count.next_sibling()); // remove all non-element nodes following a count node + + HeadingPairsNode.remove_child(count); } - // ===== Apply some pretty formatting by removing all whitespace nodes before pair name. - while (item.previous_sibling().type() == pugi::node_pcdata) HeadingPairsNode.remove_child(item.previous_sibling()); + // REMOVED: formatting doesn't get prettier by removing whitespaces on both sides of a pair + // while (item.previous_sibling().type() == pugi::node_pcdata) HeadingPairsNode.remove_child(item.previous_sibling()); HeadingPairsNode.remove_child(item); headingPairsSize(xmlDocument().document_element()).set_value(HeadingPairsNode.child_count_of_type()); @@ -266,18 +408,18 @@ void XLAppProperties::deleteHeadingPair(const std::string& name) */ void XLAppProperties::setHeadingPair(const std::string& name, int newValue) { - if (!m_xmlData) return; + if (m_xmlData == nullptr) return; const XMLNode HeadingPairsNode = headingPairsNode(xmlDocument().document_element()); XMLNode item = HeadingPairsNode.first_child_of_type(pugi::node_element); - while (item && item.first_child_of_type(pugi::node_element).child_value() != name) + while (not item.empty() && item.first_child_of_type(pugi::node_element).child_value() != name) item = item.next_sibling_of_type(pugi::node_element) .next_sibling_of_type(pugi::node_element); // advance two elements to skip count node - if (item) { - const XMLNode pairCountValue = item.next_sibling_of_type(pugi::node_element).first_child_of_type(pugi::node_element); // + if (not item.empty()) { + XMLNode pairCountValue = item.next_sibling_of_type(pugi::node_element).first_child_of_type(pugi::node_element); using namespace std::literals::string_literals; - if (pairCountValue && (pairCountValue.name() == "vt:i4"s)) + if (not pairCountValue.empty() && (pairCountValue.name() == "vt:i4"s)) pairCountValue.text().set(std::to_string(newValue).c_str()); else throw XLInternalError("XLAppProperties::setHeadingPair: found no matching pair count value to name "s + name); @@ -289,9 +431,9 @@ void XLAppProperties::setHeadingPair(const std::string& name, int newValue) */ void XLAppProperties::setProperty(const std::string& name, const std::string& value) { - if (!m_xmlData) return; - const auto property = xmlDocument().document_element().child(name.c_str()); - if (!property) xmlDocument().document_element().append_child(name.c_str()); + if (m_xmlData == nullptr) return; + auto property = xmlDocument().document_element().child(name.c_str()); + if (property.empty()) xmlDocument().document_element().append_child(name.c_str()); property.text().set(value.c_str()); } @@ -300,11 +442,15 @@ void XLAppProperties::setProperty(const std::string& name, const std::string& va */ std::string XLAppProperties::property(const std::string& name) const { - if (!m_xmlData) return ""; - const auto property = xmlDocument().document_element().child(name.c_str()); - if (!property) xmlDocument().document_element().append_child(name.c_str()); - + if (m_xmlData == nullptr) return ""; + XMLNode property = xmlDocument().document_element().child(name.c_str()); + if (property.empty()) + property = xmlDocument().document_element().append_child(name.c_str()); // BUGFIX 2024-05-21: re-assign the newly created node to + // property, so that .text().get() is defined behavior return property.text().get(); + // NOTE 2024-05-21: this was previously defined behavior because XMLNode::text() called from an empty xml_node returns an xml_text node + // constructed on an empty _root pointer, while in turn xml_text::get() returns a PUGIXML_TEXT("") for an empty xml_text node + // However, relying on all this implicit functionality was really ugly ;) } /** @@ -312,9 +458,9 @@ std::string XLAppProperties::property(const std::string& name) const */ void XLAppProperties::deleteProperty(const std::string& name) { - if (!m_xmlData) return; + if (m_xmlData == nullptr) return; const auto property = xmlDocument().document_element().child(name.c_str()); - if (!property) return; + if (property.empty()) return; xmlDocument().document_element().remove_child(property); } @@ -324,8 +470,8 @@ void XLAppProperties::deleteProperty(const std::string& name) */ void XLAppProperties::appendSheetName(const std::string& sheetName) { - if (!m_xmlData) return; - const auto theNode = sheetNames(xmlDocument().document_element()).append_child("vt:lpstr"); + if (m_xmlData == nullptr) return; + auto theNode = sheetNames(xmlDocument().document_element()).append_child("vt:lpstr"); theNode.text().set(sheetName.c_str()); sheetCount(xmlDocument().document_element()).set_value(sheetCount(xmlDocument().document_element()).as_uint() + 1); } @@ -335,8 +481,8 @@ void XLAppProperties::appendSheetName(const std::string& sheetName) */ void XLAppProperties::prependSheetName(const std::string& sheetName) { - if (!m_xmlData) return; - const auto theNode = sheetNames(xmlDocument().document_element()).prepend_child("vt:lpstr"); + if (m_xmlData == nullptr) return; + auto theNode = sheetNames(xmlDocument().document_element()).prepend_child("vt:lpstr"); theNode.text().set(sheetName.c_str()); sheetCount(xmlDocument().document_element()).set_value(sheetCount(xmlDocument().document_element()).as_uint() + 1); } @@ -346,7 +492,7 @@ void XLAppProperties::prependSheetName(const std::string& sheetName) */ void XLAppProperties::insertSheetName(const std::string& sheetName, unsigned int index) { - if (!m_xmlData) return; + if (m_xmlData == nullptr) return; if (index <= 1) { prependSheetName(sheetName); @@ -358,8 +504,8 @@ void XLAppProperties::insertSheetName(const std::string& sheetName, unsigned int // ===== If at least one sheet node exists, apply some pretty formatting by appending new sheet name between last sheet and trailing // whitespaces. const XMLNode lastSheet = sheetNames(xmlDocument().document_element()).last_child_of_type(pugi::node_element); - if (lastSheet) { - const XMLNode theNode = sheetNames(xmlDocument().document_element()).insert_child_after("vt:lpstr", lastSheet); + if (not lastSheet.empty()) { + XMLNode theNode = sheetNames(xmlDocument().document_element()).insert_child_after("vt:lpstr", lastSheet); theNode.text().set(sheetName.c_str()); // ===== Update sheet count before return statement. sheetCount(xmlDocument().document_element()).set_value(sheetCount(xmlDocument().document_element()).as_uint() + 1); @@ -371,18 +517,18 @@ void XLAppProperties::insertSheetName(const std::string& sheetName, unsigned int XMLNode curNode = sheetNames(xmlDocument().document_element()).first_child_of_type(pugi::node_element); unsigned idx = 1; - while (curNode) { + while (not curNode.empty()) { if (idx == index) break; curNode = curNode.next_sibling_of_type(pugi::node_element); ++idx; } - if (!curNode) { + if (curNode.empty()) { appendSheetName(sheetName); return; } - const XMLNode theNode = sheetNames(xmlDocument().document_element()).insert_child_before("vt:lpstr", curNode); + XMLNode theNode = sheetNames(xmlDocument().document_element()).insert_child_before("vt:lpstr", curNode); theNode.text().set(sheetName.c_str()); sheetCount(xmlDocument().document_element()).set_value(sheetCount(xmlDocument().document_element()).as_uint() + 1); diff --git a/OpenXLSX/sources/XLRelationships.cpp b/OpenXLSX/sources/XLRelationships.cpp index cc244e9..1c7e3c4 100644 --- a/OpenXLSX/sources/XLRelationships.cpp +++ b/OpenXLSX/sources/XLRelationships.cpp @@ -44,7 +44,12 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. */ // ===== External Includes ===== // +#include // uint32_t +#include // std::make_unique #include +#include // std::invalid_argument +#include // std::stoi, std::literals::string_literals +#include // std::vector // ===== OpenXLSX Includes ===== // #include "XLDocument.hpp" @@ -108,6 +113,10 @@ namespace return type; } +} // namespace + +namespace OpenXLSX_XLRelationships { // make GetStringFromType accessible throughout the project (for use by XLAppProperties) + using namespace OpenXLSX; std::string GetStringFromType(XLRelationshipType type) { std::string typeString; @@ -159,19 +168,21 @@ namespace return typeString; } +} // namespace OpenXLSX_XLRelationships +namespace { // re-open anonymous namespace uint32_t GetNewRelsID(XMLNode relationshipsNode) { using namespace std::literals::string_literals; // ===== workaround for pugi::xml_node currently not having an iterator for node_element only XMLNode relationship = relationshipsNode.first_child_of_type(pugi::node_element); uint32_t newId = 1; // default - while (!relationship.empty()) { + while (not relationship.empty()) { uint32_t id; try { id = std::stoi(std::string(relationship.attribute("Id").value()).substr(3)); } - catch (std::invalid_argument e) { // expected stoi exception + catch (std::invalid_argument const & e) { // expected stoi exception throw XLInputError("GetNewRelsID could not convert attribute Id to uint32_t ("s + e.what() + ")"s); } catch (...) { // catch all other errors during conversion of attribute to uint32_t @@ -181,18 +192,6 @@ namespace relationship = relationship.next_sibling_of_type(pugi::node_element); } return newId; - // ===== if a node_element iterator can be implemented for pugi::xml_node, the below code can be used again - // return static_cast(stoi(std::string(std::max_element(relationshipsNode.children().begin(), - // relationshipsNode.children().end(), - // [](XMLNode a, XMLNode b) { - // return stoi(std::string(a.attribute("Id").value()).substr(3)) - // < - // stoi(std::string(b.attribute("Id").value()).substr(3)); - // }) - // ->attribute("Id") - // .value()) - // .substr(3)) + - // 1); } } // namespace @@ -254,14 +253,14 @@ XLRelationshipItem XLRelationships::relationshipByTarget(const std::string& targ } /** - * @details Returns a const reference to the internal datastructure (std::map) + * @details Returns a const reference to the internal datastructure (std::vector) */ std::vector XLRelationships::relationships() const { // ===== workaround for pugi::xml_node currently not having an iterator for node_element only auto result = std::vector(); XMLNode item = xmlDocument().document_element().first_child_of_type(pugi::node_element); - while (!item.empty()) { + while (not item.empty()) { result.emplace_back(XLRelationshipItem(item)); item = item.next_sibling_of_type(pugi::node_element); } @@ -284,13 +283,13 @@ void XLRelationships::deleteRelationship(const XLRelationshipItem& item) { delet */ XLRelationshipItem XLRelationships::addRelationship(XLRelationshipType type, const std::string& target) { - const std::string typeString = GetStringFromType(type); + const std::string typeString = OpenXLSX_XLRelationships::GetStringFromType(type); const std::string id = "rId" + std::to_string(GetNewRelsID(xmlDocument().document_element())); // Create new node in the .rels file - XMLNode node = xmlDocument().document_element().last_child_of_type( - pugi::node_element); // pretty formatting: insert new node before whitespaces following last element node - if (node) + XMLNode node = xmlDocument().document_element().last_child_of_type(pugi::node_element); // pretty formatting: insert new node before + // whitespaces following last element node + if (not node.empty()) node = xmlDocument().document_element().insert_child_after("Relationship", node); else node = xmlDocument().document_element().append_child("Relationship"); diff --git a/OpenXLSX/sources/XLRow.cpp b/OpenXLSX/sources/XLRow.cpp index 6569fd6..371d106 100644 --- a/OpenXLSX/sources/XLRow.cpp +++ b/OpenXLSX/sources/XLRow.cpp @@ -154,13 +154,13 @@ namespace OpenXLSX void XLRow::setHeight(float height) // NOLINT { // Set the 'ht' attribute for the Cell. If it does not exist, create it. - if (!m_rowNode->attribute("ht")) + if (m_rowNode->attribute("ht").empty()) m_rowNode->append_attribute("ht") = height; else m_rowNode->attribute("ht").set_value(height); // Set the 'customHeight' attribute. If it does not exist, create it. - if (!m_rowNode->attribute("customHeight")) + if (m_rowNode->attribute("customHeight").empty()) m_rowNode->append_attribute("customHeight") = 1; else m_rowNode->attribute("customHeight").set_value(1); @@ -184,7 +184,7 @@ namespace OpenXLSX void XLRow::setDescent(float descent) { // Set the 'x14ac:dyDescent' attribute. If it does not exist, create it. - if (!m_rowNode->attribute("x14ac:dyDescent")) + if (m_rowNode->attribute("x14ac:dyDescent").empty()) m_rowNode->append_attribute("x14ac:dyDescent") = descent; else m_rowNode->attribute("x14ac:dyDescent") = descent; @@ -205,7 +205,7 @@ namespace OpenXLSX void XLRow::setHidden(bool state) // NOLINT { // Set the 'hidden' attribute. If it does not exist, create it. - if (!m_rowNode->attribute("hidden")) + if (m_rowNode->attribute("hidden").empty()) m_rowNode->append_attribute("hidden") = static_cast(state); else m_rowNode->attribute("hidden").set_value(static_cast(state)); @@ -275,8 +275,10 @@ namespace OpenXLSX bool XLRow::isEqual(const XLRow& lhs, const XLRow& rhs) { - if (lhs.m_rowNode && !rhs.m_rowNode) return false; - if (!lhs.m_rowNode && !rhs.m_rowNode) return true; + // 2024-05-28 BUGFIX: (!lhs.m_rowNode && rhs.m_rowNode) was not evaluated, triggering a segmentation fault on dereferencing + if (static_cast(lhs.m_rowNode) != static_cast(rhs.m_rowNode)) return false; + // ===== If execution gets here, row nodes are BOTH valid or BOTH invalid / empty + if (not lhs.m_rowNode) return true; // checking one for being empty is enough to know both are empty return *lhs.m_rowNode == *rhs.m_rowNode; } diff --git a/OpenXLSX/sources/XLRowData.cpp b/OpenXLSX/sources/XLRowData.cpp index 841ba2b..ddc0ca8 100644 --- a/OpenXLSX/sources/XLRowData.cpp +++ b/OpenXLSX/sources/XLRowData.cpp @@ -60,10 +60,13 @@ namespace OpenXLSX * @details Constructor. * @pre The given range and location are both valid. * @post + * @note 2024-05-28: added support for constructing with an empty m_cellNode from an empty rowDataRange which will allow obtaining + * an XLIteratorLocation::End for such a range so that iterations can fail in a controlled manner */ XLRowDataIterator::XLRowDataIterator(const XLRowDataRange& rowDataRange, XLIteratorLocation loc) : m_dataRange(std::make_unique(rowDataRange)), - m_cellNode(std::make_unique(getCellNode(*m_dataRange->m_rowNode, m_dataRange->m_firstCol))), + m_cellNode(std::make_unique( + getCellNode((m_dataRange->size() ? *m_dataRange->m_rowNode : XMLNode {}), m_dataRange->m_firstCol))), m_currentCell(loc == XLIteratorLocation::End ? XLCell() : XLCell(*m_cellNode, m_dataRange->m_sharedStrings)) {} @@ -129,15 +132,10 @@ namespace OpenXLSX // ===== m_currentCell is set to an empty XLCell, indicating the end of the range has been reached. if (cellNumber > m_dataRange->m_lastCol) m_currentCell = XLCell(); - // ====== If the cellNode is null (i.e. no more children in the current row node) or the column number of the cell node + // ====== If the cellNode is empty (i.e. no more children in the current row node) or the column number of the cell node // ====== is higher than the computed column number, then insert the node. - // TODO: When checking for > cellNumber rather than != cellNumber, m_cellNode->empty() fails. Why? - // TODO: Apparently only fails when assigning containers with POD values, rather XLCellValues. - // else if (m_cellNode->empty() || XLCellReference(cellNode.attribute("r").value()).column() > cellNumber) { - // BUG BUGFIX 2024-04-26: check was for m_cellNode->empty(), allowing an invalid test for the attribute r, discovered // because the modified XLCellReference throws an exception on invalid parameter - // this BUGFIX should explain the TODO above else if (cellNode.empty() || XLCellReference(cellNode.attribute("r").value()).column() > cellNumber) { cellNode = m_dataRange->m_rowNode->insert_child_after("c", *m_currentCell.m_cellNode); cellNode.append_attribute("r").set_value( @@ -190,8 +188,10 @@ namespace OpenXLSX */ bool XLRowDataIterator::operator==(const XLRowDataIterator& rhs) const { - if (m_currentCell && !rhs.m_currentCell) return false; - if (!m_currentCell && !rhs.m_currentCell) return true; + // 2024-05-28 BUGFIX: (!m_currentCell && rhs.m_currentCell) was not evaluated, triggering a segmentation fault on dereferencing + if (static_cast(m_currentCell) != static_cast(rhs.m_currentCell)) return false; + // ===== If execution gets here, current cells are BOTH valid or BOTH invalid / empty + if (not m_currentCell) return true; // checking one for being empty is enough to know both are empty return m_currentCell == rhs.m_currentCell; } @@ -231,7 +231,7 @@ namespace OpenXLSX * exception. */ XLRowDataRange::XLRowDataRange() - : m_rowNode(), + : m_rowNode(nullptr), m_firstCol(1), // first col of 1 m_lastCol(0), // and last col of 0 will ensure that size returns 0 m_sharedStrings() @@ -245,7 +245,7 @@ namespace OpenXLSX * @post */ XLRowDataRange::XLRowDataRange(const XLRowDataRange& other) - : m_rowNode(std::make_unique(*other.m_rowNode)), + : m_rowNode((other.m_rowNode != nullptr) ? std::make_unique(*other.m_rowNode) : nullptr), // 2024-05-28: support for copy-construction from an empty XLDataRange m_firstCol(other.m_firstCol), m_lastCol(other.m_lastCol), m_sharedStrings(other.m_sharedStrings) @@ -299,8 +299,9 @@ namespace OpenXLSX * @details Get an iterator to the first cell in the range. * @pre * @post + * @note 2024-05-28: enhanced ::begin() to return an end iterator for an empty range */ - XLRowDataIterator XLRowDataRange::begin() { return XLRowDataIterator { *this, XLIteratorLocation::Begin }; } + XLRowDataIterator XLRowDataRange::begin() { return XLRowDataIterator { *this, (size() > 0 ? XLIteratorLocation::Begin : XLIteratorLocation::End) }; } /** * @details Get an iterator to (one past) the last cell in the range. @@ -457,7 +458,7 @@ namespace OpenXLSX if (numCells > 0) { XMLNode node = lastElementChild; // avoid unneeded call to first_child_of_type by iterating backwards, vector is random // access so it doesn't matter - while (!node.empty()) { + while (not node.empty()) { result[XLCellReference(node.attribute("r").value()).column() - 1] = XLCell(node, m_row->m_sharedStrings).value(); node = node.previous_sibling_of_type(pugi::node_element); } @@ -486,7 +487,7 @@ namespace OpenXLSX // ===== Mark cell nodes for deletion std::vector toBeDeleted; XMLNode cellNode = m_rowNode->first_child_of_type(pugi::node_element); - while (!cellNode.empty()) { + while (not cellNode.empty()) { if (XLCellReference(cellNode.attribute("r").value()).column() <= count) { toBeDeleted.emplace_back(cellNode); XMLNode nextNode = cellNode.next_sibling(); // get next "regular" sibling (any type) before advancing cellNode @@ -515,11 +516,14 @@ namespace OpenXLSX */ void XLRowDataProxy::prependCellValue(const XLCellValue& value, uint16_t col) // NOLINT // 2024-04-30: whitespace support { - // XMLNode first_child = m_rowNode->first_child_of_type(pugi::node_element); // pretty formatting by inserting before an existing - // first child XMLNode curNode{}; if (first_child.empty()) + // ===== (disabled) Pretty formatting by inserting before an existing first child + // XMLNode first_child = m_rowNode->first_child_of_type(pugi::node_element); + // XMLNode curNode{}; + // if (first_child.empty()) // curNode = m_rowNode->prepend_child("c"); // else // curNode = m_rowNode->insert_child_before("c", first_child); + auto curNode = m_rowNode->prepend_child("c"); // this will correctly insert a new cell directly at the beginning of the row curNode.append_attribute("r").set_value(XLCellReference(static_cast(m_row->rowNumber()), col).address().c_str()); XLCell(curNode, m_row->m_sharedStrings).value() = value; diff --git a/OpenXLSX/sources/XLSharedStrings.cpp b/OpenXLSX/sources/XLSharedStrings.cpp index 389900e..43db9c9 100644 --- a/OpenXLSX/sources/XLSharedStrings.cpp +++ b/OpenXLSX/sources/XLSharedStrings.cpp @@ -85,7 +85,7 @@ bool XLSharedStrings::stringExists(const std::string& str) const { return getStr /** * @details */ -const char* XLSharedStrings::getString(uint32_t index) const +const char* XLSharedStrings::getString(int32_t index) const { if (index >= m_stringCache->size()) { // 2024-04-30: added range check using namespace std::literals::string_literals; @@ -100,26 +100,32 @@ const char* XLSharedStrings::getString(uint32_t index) const */ int32_t XLSharedStrings::appendString(const std::string& str) { + // size_t stringCacheSize = std::distance(m_stringCache->begin(), m_stringCache->end()); // any reason why .size() would not work? + size_t stringCacheSize = m_stringCache->size(); // 2024-05-31: analogous with already added range check in getString + if (stringCacheSize >= XLMaxSharedStrings) { // 2024-05-31: added range check + using namespace std::literals::string_literals; + throw XLInternalError("XLSharedStrings::"s + __func__ + ": exceeded max strings count "s + std::to_string(XLMaxSharedStrings)); + } auto textNode = xmlDocument().document_element().append_child("si").append_child("t"); if ((!str.empty()) && (str.front() == ' ' || str.back() == ' ')) textNode.append_attribute("xml:space").set_value("preserve"); // pull request #161 textNode.text().set(str.c_str()); - m_stringCache->emplace_back(textNode.text().get()); + m_stringCache->emplace_back(textNode.text().get()); // index of this element = previous stringCacheSize - return static_cast(std::distance(m_stringCache->begin(), m_stringCache->end()) - 1); + return static_cast(stringCacheSize); } /** * @details Print the underlying XML using pugixml::xml_node::print */ -void XLSharedStrings::print(std::basic_ostream>& ostr) { xmlDocument().document_element().print(ostr); } +void XLSharedStrings::print(std::basic_ostream& ostr) const { xmlDocument().document_element().print(ostr); } /** * @details Clear the string at the given index. This will affect the entire spreadsheet; everywhere the shared string * is used, it will be erased. + * @note: 2024-05-31 DONE: index now int32_t everywhere, 2 billion shared strings should be plenty */ -// 2024-04-30 CAUTION: other functions use uint32_t index or even int32_t (getStringIndex) - should be pulled straight! -void XLSharedStrings::clearString(uint64_t index) // 2024-04-30: whitespace support +void XLSharedStrings::clearString(int32_t index) // 2024-04-30: whitespace support { if (index >= m_stringCache->size()) // 2024-04-30: added range check throw XLInternalError(std::string("XLSharedStrings::") + std::string(__func__) + std::string(": index ") + std::to_string(index) + @@ -129,16 +135,20 @@ void XLSharedStrings::clearString(uint64_t index) // 2024-04-30: whitespace s // auto iter = xmlDocument().document_element().children().begin(); // std::advance(iter, index); // iter->text().set(""); // 2024-04-30: BUGFIX: this was never going to work, entries can be plenty that need to be cleared, - // including formatting 2024-04-30 CAUTION: performance critical - with whitespace support, the function can no longer know the exact - // iterator position of the shared string to be cleared TBD what to do instead? potential solution: store the XML child position with - // each entry in m_stringCache in a std::deque with struct entry { std::string s; uint64_t xmlChildIndex; }; + // including formatting + + /* 2024-04-30 CAUTION: performance critical - with whitespace support, the function can no longer know the exact iterator position of + * the shared string to be cleared - TBD what to do instead? + * Potential solution: store the XML child position with each entry in m_stringCache in a std::deque + * with struct entry { std::string s; uint64_t xmlChildIndex; }; + */ XMLNode sharedStringNode = xmlDocument().document_element().first_child_of_type(pugi::node_element); uint64_t sharedStringPos = 0; - while (sharedStringPos < index && !sharedStringNode.empty()) { + while (sharedStringPos < index && not sharedStringNode.empty()) { sharedStringNode = sharedStringNode.next_sibling_of_type(pugi::node_element); ++sharedStringPos; } - if (!sharedStringNode.empty()) { // index was found + if (not sharedStringNode.empty()) { // index was found sharedStringNode.remove_children(); // clear all data and formatting sharedStringNode.append_child("t"); // append an empty text node } diff --git a/OpenXLSX/sources/XLSheet.cpp b/OpenXLSX/sources/XLSheet.cpp index 1f8344b..7bd9840 100644 --- a/OpenXLSX/sources/XLSheet.cpp +++ b/OpenXLSX/sources/XLSheet.cpp @@ -240,7 +240,8 @@ XLSheet::operator XLChartsheet() const { return this->get(); } /** * @details Print the underlying XML using pugixml::xml_node::print */ -void XLSheet::print(std::basic_ostream>& ostr) { xmlDocument().document_element().print(ostr); } +void XLSheet::print(std::basic_ostream& ostr) const { xmlDocument().document_element().print( ostr ); } + // ========== XLWorksheet Member Functions @@ -261,7 +262,7 @@ XLWorksheet::XLWorksheet(XLXmlData* xmlData) : XLSheetBase(xmlData) // If Column properties are grouped, divide them into properties for individual Columns. if (xmlDocument().document_element().child("cols").type() != pugi::node_null) { auto currentNode = xmlDocument().document_element().child("cols").first_child_of_type(pugi::node_element); - while (!currentNode.empty()) { + while (not currentNode.empty()) { uint16_t min {}; uint16_t max {}; try { @@ -276,7 +277,7 @@ XLWorksheet::XLWorksheet(XLXmlData* xmlData) : XLSheetBase(xmlData) for (uint16_t i = min; i < max; i++) { // NOLINT auto newnode = xmlDocument().document_element().child("cols").insert_child_before("col", currentNode); auto attr = currentNode.first_attribute(); - while (!attr.empty()) { // NOLINT + while (not attr.empty()) { // NOLINT newnode.append_attribute(attr.name()) = attr.value(); attr = attr.next_attribute(); } @@ -338,8 +339,7 @@ bool XLWorksheet::setActive_impl() */ XLCellAssignable XLWorksheet::cell(const std::string& ref) const { - return static_cast( - cell(XLCellReference(ref))); // TODO TBD if this is defined behavior: XLCellAssignable adds only methods, no member variables + return XLCellAssignable(cell(XLCellReference(ref))); // move-construct XLCellAssignable from temporary XLCell } /** @@ -357,50 +357,7 @@ XLCell XLWorksheet::cell(uint32_t rowNumber, uint16_t columnNumber) const const XMLNode cellNode = getCellNode(rowNode, columnNumber, rowNumber); return XLCell { cellNode, parentDoc().execQuery(XLQuery(XLQueryType::QuerySharedStrings)).result() }; - /*** BEGIN obsolete section: below code is identical to XLUtilities.hpp::getCellNode ***/ - // // ===== Get the last child of rowNode that is of type node_element, if any. - // auto cellNode = XMLNode(); - // cellNode = rowNode.last_child_of_type(pugi::node_element); - // auto cellRef = XLCellReference(rowNumber, columnNumber); // only distinction from getCellNode: rowNumber is available directly - // as uint16_t - // - // // ===== If there are no cells in the current row, or the requested cell is beyond the last cell in the row... - // if (cellNode.empty() || (XLCellReference(cellNode.attribute("r").value()).column() < columnNumber)) { - // // ===== append a new node to the end. - // rowNode.append_child("c").append_attribute("r").set_value(cellRef.address().c_str()); - // cellNode = rowNode.last_child(); - // } - // // ===== If the requested node is closest to the end, start from the end and search backwards... - // else if (XLCellReference(cellNode.attribute("r").value()).column() - columnNumber < columnNumber) { - // while (!cellNode.empty() && (XLCellReference(cellNode.attribute("r").value()).column() > columnNumber)) cellNode = - // cellNode.previous_sibling_of_type(pugi::node_element); - // // ===== If the backwards search failed to locate the requested cell - // if (cellNode.empty() || (XLCellReference(cellNode.attribute("r").value()).column() < columnNumber)) { - // if (cellNode.empty()) // If between row begin and higher column number, only non-element nodes exist - // cellNode = rowNode.prepend_child("c"); // insert a new cell node at row begin. When saving, this will keep whitespace - // formatting towards next cell node - // else - // cellNode = rowNode.insert_child_after("c", cellNode); - // cellNode.append_attribute("r").set_value(cellRef.address().c_str()); - // } - // } - // // ===== Otherwise, start from the beginning - // else { - // // ===== At this point, it is guaranteed that there is at least one node_element in the row that is not empty. - // cellNode = rowNode.first_child_of_type(pugi::node_element); - // - // // ===== It has been verified above that the requested columnNumber is <= the column number of the last node_element, - // therefore this loop will halt: while (XLCellReference(cellNode.attribute("r").value()).column() < columnNumber) cellNode = - // cellNode.next_sibling_of_type(pugi::node_element); - // // ===== If the forwards search failed to locate the requested cell - // if (XLCellReference(cellNode.attribute("r").value()).column() > columnNumber) { - // cellNode = rowNode.insert_child_before("c", cellNode); - // cellNode.append_attribute("r").set_value(cellRef.address().c_str()); - // } - // } - // - // return XLCell{cellNode, parentDoc().execQuery(XLQuery(XLQueryType::QuerySharedStrings)).result()}; - /*** END obsolete section ***/ + /*** removed obsolete section with code identical to XLUtilities.hpp::getCellNode ***/ } /** @@ -493,20 +450,22 @@ XLColumn XLWorksheet::column(uint16_t columnNumber) const uint16_t minColumn {}; uint16_t maxColumn {}; - if (!columnNode.empty()) { + if (not columnNode.empty()) { minColumn = columnNode.attribute("min").as_int(); // only look it up once for multiple access maxColumn = columnNode.attribute("max").as_int(); // " } // ===== If the node exists for the column, and only spans that column, then continue... - if (!columnNode.empty() && (minColumn == columnNumber) && (maxColumn == columnNumber)) { + if (not columnNode.empty() && (minColumn == columnNumber) && (maxColumn == columnNumber)) { } // ===== If the node exists for the column, but spans several columns, split it into individual nodes, and set columnNode to the right - // one... BUGFIX 2024-04-27 - old if condition would split a multi-column setting even if columnNumber is < minColumn (see lambda return - // value above) NOTE 2024-04-27: the column splitting for loaded files is already handled in the constructor, technically this code is - // not necessary here - else if (!columnNode.empty() && (columnNumber >= minColumn) && (minColumn != maxColumn)) { + // one... + // BUGFIX 2024-04-27 - old if condition would split a multi-column setting even if columnNumber is < minColumn (see lambda return value + // above) + // NOTE 2024-04-27: the column splitting for loaded files is already handled in the constructor, technically this code is not necessary + // here + else if (not columnNode.empty() && (columnNumber >= minColumn) && (minColumn != maxColumn)) { // ===== Split the node in individual columns... columnNode.attribute("min").set_value(maxColumn); // Limit the original node to a single column for (int i = minColumn; i < maxColumn; ++i) { @@ -518,7 +477,8 @@ XLColumn XLWorksheet::column(uint16_t columnNumber) const // min = max attribute // // ===== Delete the original node // columnNode = columnNode.previous_sibling_of_type(pugi::node_element); // due to insert loop, previous node should be guaranteed - // to be an element node xmlDocument().document_element().child("cols").remove_child(columnNode.next_sibling()); + // // to be an element node + // xmlDocument().document_element().child("cols").remove_child(columnNode.next_sibling()); // ===== Find the node corresponding to the column number - BUGFIX 2024-04-27: loop should abort on empty node while (not columnNode.empty() && columnNode.attribute("min").as_int() != columnNumber) @@ -528,8 +488,8 @@ XLColumn XLWorksheet::column(uint16_t columnNumber) const "not found after splitting column nodes"s); } - // ===== If a node for the column does NOT exist, but a node for a higher column exist... - else if (!columnNode.empty() && minColumn > columnNumber) { + // ===== If a node for the column does NOT exist, but a node for a higher column exists... + else if (not columnNode.empty() && minColumn > columnNumber) { columnNode = xmlDocument().document_element().child("cols").insert_child_before("col", columnNode); columnNode.append_attribute("min") = columnNumber; columnNode.append_attribute("max") = columnNumber; @@ -546,10 +506,11 @@ XLColumn XLWorksheet::column(uint16_t columnNumber) const columnNode.append_attribute("customWidth") = 0; } - using namespace std::literals::string_literals; - if (columnNode.empty()) + if (columnNode.empty()) { + using namespace std::literals::string_literals; throw XLInternalError("XLWorksheet::"s + __func__ + ": was unable to find or create node for column "s + std::to_string(columnNumber)); + } return XLColumn(columnNode); } @@ -569,12 +530,6 @@ uint16_t XLWorksheet::columnCount() const noexcept maxCount = std::max(cellCount, maxCount); } return maxCount; - - // std::vector counts; // Pull request: Update XLSheet.cpp with correct type #176, Explicitely cast to unsigned short int #163 - // for (const auto& row : rows()) { - // counts.emplace_back(row.cellCount()); - // } - // return std::max(static_cast(1), *std::max_element(counts.begin(), counts.end())); } /** @@ -607,7 +562,8 @@ void XLWorksheet::updateSheetName(const std::string& oldName, const std::string& // ===== Iterate through all defined names XMLNode row = xmlDocument().document_element().child("sheetData").first_child_of_type(pugi::node_element); for (; not row.empty(); row = row.next_sibling_of_type(pugi::node_element)) { - for (XMLNode cell = row.first_child_of_type(pugi::node_element); not cell.empty(); + for (XMLNode cell = row.first_child_of_type(pugi::node_element); + not cell.empty(); cell = cell.next_sibling_of_type(pugi::node_element)) { if (!XLCell(cell, XLSharedStrings()).hasFormula()) continue; diff --git a/OpenXLSX/sources/XLWorkbook.cpp b/OpenXLSX/sources/XLWorkbook.cpp index b43a5aa..53101ea 100644 --- a/OpenXLSX/sources/XLWorkbook.cpp +++ b/OpenXLSX/sources/XLWorkbook.cpp @@ -115,7 +115,8 @@ XLSheet XLWorkbook::sheet(uint16_t index) // 2024-04-30: whitespace support // ===== Find the n-th node_element that corresponds to index uint16_t curIndex = 0; - for (XMLNode node = sheetsNode(xmlDocument()).first_child_of_type(pugi::node_element); node.empty() == false; + for (XMLNode node = sheetsNode(xmlDocument()).first_child_of_type(pugi::node_element); + not node.empty(); node = node.next_sibling_of_type(pugi::node_element)) { if (++curIndex == index) return sheet(node.attribute("name").as_string()); @@ -178,8 +179,8 @@ void XLWorkbook::deleteNamedRanges() /** * @details */ -void XLWorkbook::deleteSheet(const std::string& sheetName) // 2024-05-02: whitespace support - CAUTION: execCommand on underlying XML - // with whitespaces not verified +void XLWorkbook::deleteSheet(const std::string& sheetName) // 2024-05-02: whitespace support + // CAUTION: execCommand on underlying XML with whitespaces not verified { // ===== Determine ID and type of sheet, as well as current worksheet count. auto sheetID = sheetsNode(xmlDocument()).find_child_by_attribute("name", sheetName.c_str()).attribute("r:id").value(); // NOLINT @@ -205,15 +206,14 @@ void XLWorkbook::deleteSheet(const std::string& sheetName) // 2024-05-02: whi parentDoc().execCommand( XLCommand(XLCommandType::DeleteSheet).setParam("sheetID", std::string(sheetID)).setParam("sheetName", sheetName)); XMLNode sheet = sheetsNode(xmlDocument()).find_child_by_attribute("name", sheetName.c_str()); - if (!sheet.empty()) { - // ===== Delete all non element nodes (comments, whitespaces) following the sheet that is being deleted from workbook.xml - // node + if (not sheet.empty()) { + // ===== Delete all non element nodes (comments, whitespaces) following the sheet being deleted from workbook.xml node XMLNode nonElementNode = sheet.next_sibling(); - while (!nonElementNode.empty() && nonElementNode.type() != pugi::node_element) { + while (not nonElementNode.empty() && nonElementNode.type() != pugi::node_element) { sheetsNode(xmlDocument()).remove_child(nonElementNode); nonElementNode = nonElementNode.next_sibling(); } - sheetsNode(xmlDocument()).remove_child(sheet); // delete the actual sheet entry in + sheetsNode(xmlDocument()).remove_child(sheet); // delete the actual sheet entry } if (sheetIsActive(sheetID)) @@ -242,6 +242,8 @@ void XLWorkbook::addWorksheet(const std::string& sheetName) /** * @details * @todo If the original sheet's tabSelected attribute is set, ensure it is un-set in the clone. + * TBD: See comment in XLWorkbook::setSheetActive - should the tabSelected actually be un-set? It's not the same as the active tab, + * which does not need to be selected */ void XLWorkbook::cloneSheet(const std::string& existingName, const std::string& newName) { @@ -255,7 +257,7 @@ uint16_t XLWorkbook::createInternalSheetID() // 2024-04-30: whitespace suppor { XMLNode sheet = xmlDocument().document_element().child("sheets").first_child_of_type(pugi::node_element); uint32_t maxSheetIdFound = 0; - while (!sheet.empty()) { + while (not sheet.empty()) { uint32_t thisSheetId = sheet.attribute("sheetId").as_uint(); if (thisSheetId > maxSheetIdFound) maxSheetIdFound = thisSheetId; sheet = sheet.next_sibling_of_type(pugi::node_element); @@ -325,7 +327,7 @@ void XLWorkbook::setSheetVisibility(const std::string& sheetRID, const std::stri int visibleSheets = 0; // for (const auto& item : xmlDocument().document_element().child("sheets").children()) { XMLNode item = xmlDocument().document_element().child("sheets").first_child_of_type(pugi::node_element); - while (!item.empty()) { + while (not item.empty()) { if (std::string(item.attribute("r:id").value()) != sheetRID) { if (isVisible(item)) ++visibleSheets; } @@ -364,7 +366,7 @@ void XLWorkbook::setSheetVisibility(const std::string& sheetRID, const std::stri if (hideSheet && activeTabIndex == index) { // BUGFIX 2024-04-30: previously, the active tab was re-set even if the current sheet was // being set to "visible" (when already being visible) XMLNode item = xmlDocument().document_element().child("sheets").first_child_of_type(pugi::node_element); - while (!item.empty()) { + while (not item.empty()) { if (isVisible(item)) { // BUGFIX 2024-05-01: old check was testing state != "hidden" || != "veryHidden", which was always true activeTabAttribute.set_value(indexOfSheet(item.attribute("name").value()) - 1); @@ -377,7 +379,7 @@ void XLWorkbook::setSheetVisibility(const std::string& sheetRID, const std::stri /** * @details - * SOLVED: @todo In some cases (eg. if a sheet is moved to the position before the selected sheet), multiple sheets are selected when opened + * @done In some cases (eg. if a sheet is moved to the position before the selected sheet), multiple sheets are selected when opened * in Excel. */ void XLWorkbook::setSheetIndex(const std::string& sheetName, unsigned int index) // 2024-05-01: whitespace support @@ -391,13 +393,13 @@ void XLWorkbook::setSheetIndex(const std::string& sheetName, unsigned int index) XMLNode sheetToMove {}; // determine the sheet matching sheetName, if any unsigned int sheetToMoveIndex = 0; XMLNode existingSheet {}; // determine the sheet at index, if any - std::string activeSheet_rId {}; // determine the r:id of the sheet at activeIndex, if any + std::string activeSheet_rId {}; // determine the r:id of the sheet at activeIndex, if any unsigned int sheetIndex = 1; XMLNode curSheet = sheetsNode(xmlDocument()).first_child_of_type(pugi::node_element); int thingsToFind = (activeSheetIndex > 0) ? 3 : 2; // if there is no active tab configured, no need to search for its name - while (!curSheet.empty() && thingsToFind > 0) { // permit early loop exit when all sheets are located + while (not curSheet.empty() && thingsToFind > 0) { // permit early loop exit when all sheets are located if (sheetToMove.empty() && (curSheet.attribute("name").value() == sheetName)) { sheetToMoveIndex = sheetIndex; sheetToMove = curSheet; @@ -435,7 +437,7 @@ void XLWorkbook::setSheetIndex(const std::string& sheetName, unsigned int index) // ===== Updated defined names with worksheet scopes. TBD what this does XMLNode definedName = xmlDocument().document_element().child("definedNames").first_child_of_type(pugi::node_element); - while (!definedName.empty()) { + while (not definedName.empty()) { // TBD: is the current definedName actually associated with the sheet that was moved? definedName.attribute("localSheetId").set_value(sheetToMoveIndex - 1); definedName = definedName.next_sibling_of_type(pugi::node_element); @@ -443,9 +445,8 @@ void XLWorkbook::setSheetIndex(const std::string& sheetName, unsigned int index) // ===== Update the activeTab attribute. if ((activeSheetIndex < std::min(index, sheetToMoveIndex)) || - (activeSheetIndex > - std::max(index, sheetToMoveIndex))) // if the active sheet was not within the set of sheets affected by the move - return; // nothing to do + (activeSheetIndex > std::max(index, sheetToMoveIndex))) // if active sheet was not within the set of sheets affected by the move + return; // nothing to do if (activeSheet_rId.length() > 0) setSheetActive(activeSheet_rId); } @@ -457,7 +458,8 @@ unsigned int XLWorkbook::indexOfSheet(const std::string& sheetName) const // { // ===== Iterate through sheet nodes. When a match is found, return the index; unsigned int index = 1; - for (XMLNode sheet = sheetsNode(xmlDocument()).first_child_of_type(pugi::node_element); sheet.empty() == false; + for (XMLNode sheet = sheetsNode(xmlDocument()).first_child_of_type(pugi::node_element); + not sheet.empty(); sheet = sheet.next_sibling_of_type(pugi::node_element)) { if (sheetName == sheet.attribute("name").value()) return index; @@ -485,7 +487,8 @@ XLSheetType XLWorkbook::typeOfSheet(const std::string& sheetName) const XLSheetType XLWorkbook::typeOfSheet(unsigned int index) const // 2024-05-01: whitespace support { unsigned int thisIndex = 1; - for (XMLNode sheet = sheetsNode(xmlDocument()).first_child_of_type(pugi::node_element); not sheet.empty(); + for (XMLNode sheet = sheetsNode(xmlDocument()).first_child_of_type(pugi::node_element); + not sheet.empty(); sheet = sheet.next_sibling_of_type(pugi::node_element)) { if (thisIndex == index) return typeOfSheet(sheet.attribute("name").as_string()); @@ -502,8 +505,8 @@ XLSheetType XLWorkbook::typeOfSheet(unsigned int index) const // 2024-05-01: unsigned int XLWorkbook::sheetCount() const // 2024-04-30: whitespace support { unsigned int count = 0; - // 2024-04-30: TBD performance issue due to whitespace support - for (XMLNode node = sheetsNode(xmlDocument()).first_child_of_type(pugi::node_element); not node.empty(); + for (XMLNode node = sheetsNode(xmlDocument()).first_child_of_type(pugi::node_element); + not node.empty(); node = node.next_sibling_of_type(pugi::node_element)) ++count; return count; @@ -526,7 +529,8 @@ std::vector XLWorkbook::sheetNames() const // 2024-05-01: whites { std::vector results; - for (XMLNode item = sheetsNode(xmlDocument()).first_child_of_type(pugi::node_element); not item.empty(); + for (XMLNode item = sheetsNode(xmlDocument()).first_child_of_type(pugi::node_element); + not item.empty(); item = item.next_sibling_of_type(pugi::node_element)) results.emplace_back(item.attribute("name").value()); @@ -540,7 +544,8 @@ std::vector XLWorkbook::worksheetNames() const // 2024-05-01: wh { std::vector results; - for (XMLNode item = sheetsNode(xmlDocument()).first_child_of_type(pugi::node_element); not item.empty(); + for (XMLNode item = sheetsNode(xmlDocument()).first_child_of_type(pugi::node_element); + not item.empty(); item = item.next_sibling_of_type(pugi::node_element)) { XLQuery query(XLQueryType::QuerySheetType); @@ -559,7 +564,8 @@ std::vector XLWorkbook::chartsheetNames() const // 2024-05-01: w { std::vector results; - for (XMLNode item = sheetsNode(xmlDocument()).first_child_of_type(pugi::node_element); not item.empty(); + for (XMLNode item = sheetsNode(xmlDocument()).first_child_of_type(pugi::node_element); + not item.empty(); item = item.next_sibling_of_type(pugi::node_element)) { XLQuery query(XLQueryType::QuerySheetType); @@ -624,7 +630,7 @@ void XLWorkbook::updateSheetReferences( // ===== Iterate through all defined names // TODO 2024-05-01: verify definedNames logic XMLNode definedName = xmlDocument().document_element().child("definedNames").first_child_of_type(pugi::node_element); - for (; definedName.empty() == false; definedName = definedName.next_sibling_of_type(pugi::node_element)) { + for (; not definedName.empty(); definedName = definedName.next_sibling_of_type(pugi::node_element)) { formula = definedName.text().get(); // ===== Skip if formula contains a '[' and ']' (means that the defined refers to external workbook) @@ -643,11 +649,11 @@ void XLWorkbook::updateSheetReferences( */ void XLWorkbook::setFullCalculationOnLoad() { - auto calcPr = xmlDocument().document_element().child("calcPr"); + XMLNode calcPr = xmlDocument().document_element().child("calcPr"); auto getOrCreateAttribute = [&calcPr](const char* attributeName) { - auto attr = calcPr.attribute(attributeName); - if (!attr) attr = calcPr.append_attribute(attributeName); + XMLAttribute attr = calcPr.attribute(attributeName); + if (attr.empty()) attr = calcPr.append_attribute(attributeName); return attr; }; @@ -658,75 +664,75 @@ void XLWorkbook::setFullCalculationOnLoad() /** * @details */ -void XLWorkbook::print(std::basic_ostream>& os) { xmlDocument().document_element().print(os); } +void XLWorkbook::print(std::basic_ostream& ostr) const { xmlDocument().document_element().print (ostr); } /** * @details */ bool XLWorkbook::sheetIsActive(const std::string& sheetRID) const // 2024-04-30: whitespace support { - const XMLNode workbookView = xmlDocument().document_element().child("bookViews").first_child_of_type(pugi::node_element); - const auto activeTabAttribute = workbookView.attribute("activeTab"); - const auto activeTabIndex = (activeTabAttribute ? activeTabAttribute.as_uint() : 0); + const XMLNode workbookView = xmlDocument().document_element().child("bookViews").first_child_of_type(pugi::node_element); + const XMLAttribute activeTabAttribute = workbookView.attribute("activeTab"); + const int32_t activeTabIndex = (not activeTabAttribute.empty() ? activeTabAttribute.as_int() : -1); // 2024-05-29 BUGFIX: activeTabAttribute was being read as_uint + if (activeTabIndex == -1) return false; // 2024-05-29 early exit: no need to try and match sheetRID if there *is* no active tab - unsigned int index = 0; + int32_t index = 0; // 2024-06-04 BUGFIX: index should support -1 as 2024-05-29 change below sets it to -1 for preventing a match with activeTabIndex XMLNode item = sheetsNode(xmlDocument()).first_child_of_type(pugi::node_element); - while (!item.empty()) { + while (not item.empty()) { if (std::string(item.attribute("r:id").value()) == sheetRID) break; ++index; item = item.next_sibling_of_type(pugi::node_element); } + if (item.empty()) index = -1; // 2024-05-29: prevent a match if activeTabIndex invalidly points to a non-existing sheet return index == activeTabIndex; } /** * @details + * @done: no exception if setSheetActive fails, instead return false + * @done: fail by returning false if sheetRID is either not found or belongs to a sheet that is not visible + * @note: this makes some bug fixes from 2024-05-29 obsolete + * @note: changed behavior: attempting to setSheetActive on a non-existing or non-visible sheet will no longer unselect the active sheet */ bool XLWorkbook::setSheetActive(const std::string& sheetRID) // 2024-04-30: whitespace support { - XMLNode workbookView = xmlDocument().document_element().child("bookViews").first_child_of_type(pugi::node_element); - const auto activeTabAttribute = workbookView.attribute("activeTab"); - int32_t activeTabIndex = -1; // negative == no active tab identified - if (!activeTabAttribute.empty()) activeTabIndex = activeTabAttribute.as_int(); + XMLNode workbookView = xmlDocument().document_element().child("bookViews").first_child_of_type(pugi::node_element); + const XMLAttribute activeTabAttribute = workbookView.attribute("activeTab"); + int32_t activeTabIndex = -1; // negative == no active tab identified + if (not activeTabAttribute.empty()) activeTabIndex = activeTabAttribute.as_int(); - unsigned int index = 0; + int32_t index = 0; // index should have the same data type as activeTabIndex for comparisons XMLNode item = sheetsNode(xmlDocument()).first_child_of_type(pugi::node_element); - while (!item.empty() && (std::string(item.attribute("r:id").value()) != sheetRID)) { + while (not item.empty() && (std::string(item.attribute("r:id").value()) != sheetRID)) { ++index; item = item.next_sibling_of_type(pugi::node_element); } + // ===== 2024-06-19: Fail without action if sheet is not found or sheet is not visible + if (item.empty() || !isVisible(item)) return false; // NOTE: XLSheet XLWorkbook::sheet(uint16_t index) is using a 1-based index, while the workbookView attribute activeTab is using a // 0-based index - // ===== If an active sheet was found, but sheetRID was not found or is not the same sheet: attempt to unselect the old active sheet. - // ===== This avoids that when opening the XLSX file in an office application, multiple sheets show as selected - TBD with Kenneth if - // this is desired behavior - // TODO: take care of (currently) index 0 when no sheetRID is found + // ===== If an active sheet was found, but sheetRID is not the same sheet: attempt to unselect the old active sheet. if ((activeTabIndex != -1) && (index != activeTabIndex)) sheet(activeTabIndex + 1).setSelected(false); // see NOTE above - // ===== Attempting to set a hidden sheet active will remove the activeTab property from the workbook.xml sheets node - if (item.empty() || - !isVisible(item)) // if sheet was not found or is hidden, it can not be set active. 2024-05-01 BUGFIX: veryHidden was not checked - workbookView.remove_attribute("activeTab"); // TODO TBD: throw an exception if item.empty() ? - else { // sheetRID was found and the sheet is visible - if (workbookView.attribute("activeTab").empty()) workbookView.append_attribute("activeTab"); - workbookView.attribute("activeTab").set_value(index); - // sheet(index + 1).setSelected(true); // see NOTE above, however it appears that an active sheet does not have to be selected - return true; // success - } - return false; // default: sheet was set to active + // ===== Set the activeTab property for the workbook.xml sheets node + if (workbookView.attribute("activeTab").empty()) workbookView.append_attribute("activeTab"); + workbookView.attribute("activeTab").set_value(index); + // sheet(index + 1).setSelected(true); // it appears that an active sheet does not have to be selected + return true; // success } /** * @details evaluate a sheet node state attribute where "hidden" or "veryHidden" means not visible + * @note 2024-05-01 BUGFIX: veryHidden was not checked (in setSheetActive) */ bool XLWorkbook::isVisibleState(std::string const& state) const { return (state != "hidden" && state != "veryHidden"); } /** - * @details function only returns meaningful information when used with a sheet node (or nodes with state attribute allowing values visible, - * hidden, veryHidden) + * @details function only returns meaningful information when used with a sheet node + * (or nodes with state attribute allowing values visible, hidden, veryHidden) */ bool XLWorkbook::isVisible(XMLNode const& sheetNode) const { diff --git a/OpenXLSX/sources/XLXmlData.cpp b/OpenXLSX/sources/XLXmlData.cpp index e444936..f132f2c 100644 --- a/OpenXLSX/sources/XLXmlData.cpp +++ b/OpenXLSX/sources/XLXmlData.cpp @@ -45,19 +45,19 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. // ===== External Includes ===== // #include +#include // ===== OpenXLSX Includes ===== // #include "XLDocument.hpp" #include "XLXmlData.hpp" -#include - using namespace OpenXLSX; +const unsigned int pugi_parse_settings = pugi::parse_default | pugi::parse_ws_pcdata; // TBD: | pugi::parse_comments /** * @details */ -XLXmlData::XLXmlData(OpenXLSX::XLDocument* parentDoc, const std::string& xmlPath, const std::string& xmlId, XLContentType xmlType) +XLXmlData::XLXmlData(XLDocument* parentDoc, const std::string& xmlPath, const std::string& xmlId, XLContentType xmlType) : m_parentDoc(parentDoc), m_xmlPath(xmlPath), m_xmlID(xmlId), @@ -77,16 +77,43 @@ XLXmlData::~XLXmlData() = default; */ void XLXmlData::setRawData(const std::string& data) // NOLINT { - m_xmlDoc->load_string(data.c_str(), pugi::parse_default | pugi::parse_ws_pcdata); + m_xmlDoc->load_string(data.c_str(), pugi_parse_settings); } /** * @details + * @note Default encoding for pugixml xml_document::save is pugi::encoding_auto, becomes pugi::encoding_utf8 */ std::string XLXmlData::getRawData() const { + XMLDocument *doc = const_cast(getXmlDocument()); + + // ===== 2024-08-08: ensure that the default encoding UTF-8 is explicitly written to the XML document with a custom saving declaration + XMLNode savingDeclaration = doc->first_child(); + if (savingDeclaration.empty() || savingDeclaration.type() != pugi::node_declaration) // if saving declaration node does not exist + savingDeclaration = doc->prepend_child(pugi::node_declaration); // create it + + // ===== If a node_declaration could be fetched or created + if (not savingDeclaration.empty()) { + // ===== Fetch or create saving declaration attributes + XMLAttribute attrVersion = savingDeclaration.attribute("version"); + if (attrVersion.empty()) + attrVersion = savingDeclaration.append_attribute("version"); + XMLAttribute attrEncoding = savingDeclaration.attribute("encoding"); + if (attrEncoding.empty()) + attrEncoding = savingDeclaration.append_attribute("encoding"); + XMLAttribute attrStandalone = savingDeclaration.attribute("standalone"); + if (attrStandalone.empty()) + attrStandalone = savingDeclaration.append_attribute("standalone"); + + // ===== Set saving declaration attribute values (potentially overwriting existing values) + attrVersion = "1.0"; // version="1.0" is XML default + attrEncoding = "UTF-8"; // encoding="UTF-8" is XML default + attrStandalone = "no"; // standalone="no" is XML default + } + std::ostringstream ostr; - getXmlDocument()->save(ostr, "", pugi::format_raw); + doc->save(ostr, "", pugi::format_raw); return ostr.str(); } @@ -136,7 +163,7 @@ XLContentType XLXmlData::getXmlType() const XMLDocument* XLXmlData::getXmlDocument() { if (!m_xmlDoc->document_element()) - m_xmlDoc->load_string(m_parentDoc->extractXmlFromArchive(m_xmlPath).c_str(), pugi::parse_default | pugi::parse_ws_pcdata); + m_xmlDoc->load_string(m_parentDoc->extractXmlFromArchive(m_xmlPath).c_str(), pugi_parse_settings); return m_xmlDoc.get(); } @@ -147,7 +174,7 @@ XMLDocument* XLXmlData::getXmlDocument() const XMLDocument* XLXmlData::getXmlDocument() const { if (!m_xmlDoc->document_element()) - m_xmlDoc->load_string(m_parentDoc->extractXmlFromArchive(m_xmlPath).c_str(), pugi::parse_default | pugi::parse_ws_pcdata); + m_xmlDoc->load_string(m_parentDoc->extractXmlFromArchive(m_xmlPath).c_str(), pugi_parse_settings); return m_xmlDoc.get(); } diff --git a/OpenXLSX/sources/XLXmlParser.cpp b/OpenXLSX/sources/XLXmlParser.cpp new file mode 100644 index 0000000..77d2e43 --- /dev/null +++ b/OpenXLSX/sources/XLXmlParser.cpp @@ -0,0 +1,171 @@ +/* + + ____ ____ ___ ____ ____ ____ ___ + 6MMMMb `MM( )M' `MM' 6MMMMb\`MM( )M' + 8P Y8 `MM. d' MM 6M' ` `MM. d' +6M Mb __ ____ ____ ___ __ `MM. d' MM MM `MM. d' +MM MM `M6MMMMb 6MMMMb `MM 6MMb `MM. d' MM YM. `MM. d' +MM MM MM' `Mb 6M' `Mb MMM9 `Mb `MMd MM YMMMMb `MMd +MM MM MM MM MM MM MM' MM dMM. MM `Mb dMM. +MM MM MM MM MMMMMMMM MM MM d'`MM. MM MM d'`MM. +YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. + 8b d8 MM. ,M9 YM d9 MM MM d' `MM. MM / L ,M9 d' `MM. + YMMMM9 MMYMMM9 YMMMM9 _MM_ _MM_M(_ _)MM_ _MMMMMMM MYMMMM9 _M(_ _)MM_ + MM + MM + _MM_ + + Copyright (c) 2018, Kenneth Troldal Balslev + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + - Neither the name of the author nor the + names of any contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + */ + +// ===== External Includes ===== // +#include + +// // ===== OpenXLSX Includes ===== // +#include "XLXmlParser.hpp" + +namespace OpenXLSX +{ + // ===== Copy definition of PUGI_IMPL_NODETYPE, which is defined in pugixml.cpp, within a namespace, and somehow doesn't work here +# define PUGI_IMPL_NODETYPE(n) static_cast((n)->header & pugi::impl::xml_memory_page_type_mask) + + /** + * @details determine the first xml_node child whose xml_node_type matches type_ + * @date 2024-04-25 + */ + XMLNode XMLNode::first_child_of_type(pugi::xml_node_type type_) const + { + if (_root) { + XMLNode x = first_child(); + XMLNode l = last_child(); + while (x != l && x.type() != type_) x = x.next_sibling(); + if (x.type() == type_) + return XMLNode(x); + } + return XMLNode(); // if no node matching type_ was found: return an empty node + } + + /** + * @details determine the last xml_node child whose xml_node_type matches type_ + * @date 2024-04-25 + */ + XMLNode XMLNode::last_child_of_type(pugi::xml_node_type type_) const + { + if (_root) { + XMLNode f = first_child(); + XMLNode x = last_child(); + while (x != f && x.type() != type_) x = x.previous_sibling(); + if (x.type() == type_) + return XMLNode(x); + } + return XMLNode(); // if no node matching type_ was found: return an empty node + } + + /** + * @details determine amount of xml_node children child whose xml_node_type matches type_ + * @date 2024-04-28 + */ + size_t XMLNode::child_count_of_type(pugi::xml_node_type type_) const + { + size_t counter = 0; + if (_root) { + XMLNode c = first_child_of_type(type_); + while (!c.empty()) { + ++counter; + c = c.next_sibling_of_type(type_); + } + } + return counter; + } + + /** + * @details determine the next xml_node sibling whose xml_node_type matches type_ + * @date 2024-04-26 + */ + XMLNode XMLNode::next_sibling_of_type(pugi::xml_node_type type_) const + { + if (_root) { + pugi::xml_node_struct* next = _root->next_sibling; + while (next && (PUGI_IMPL_NODETYPE(next) != type_)) next = next->next_sibling; + if (next) + return XMLNode(next); + } + return XMLNode(); // if no node matching type_ was found: return an empty node + } + + /** + * @details determine the previous xml_node sibling whose xml_node_type matches type_ + * @date 2024-04-26 + */ + XMLNode XMLNode::previous_sibling_of_type(pugi::xml_node_type type_) const + { + if (_root) { + pugi::xml_node_struct* prev = _root->prev_sibling_c; + while (prev->next_sibling && (PUGI_IMPL_NODETYPE(prev) != type_)) prev = prev->prev_sibling_c; + if (prev->next_sibling) + return XMLNode(prev); + } + return XMLNode(); // if no node matching type_ was found: return an empty node + } + + /** + * @details determine the next xml_node sibling whose name() matches name_ and xml_node_type matches type_ + * @date 2024-04-26 + */ + XMLNode XMLNode::next_sibling_of_type(const pugi::char_t* name_, pugi::xml_node_type type_) const + { + if (_root) { + for (pugi::xml_node_struct* i = _root->next_sibling; i; i = i->next_sibling) + { + const pugi::char_t* iname = i->name; + if (iname && pugi::impl::strequal(name_, iname) && (PUGI_IMPL_NODETYPE(i) == type_)) + return XMLNode(i); + } + } + return XMLNode(); // if no node matching type_ was found: return an empty node + } + + /** + * @details determine the previous xml_node sibling whose name() matches name_ and xml_node_type matches type_ + * @date 2024-04-26 + */ + XMLNode XMLNode::previous_sibling_of_type(const pugi::char_t* name_, pugi::xml_node_type type_) const + { + if (_root) { + for (pugi::xml_node_struct* i = _root->prev_sibling_c; i->next_sibling; i = i->prev_sibling_c) + { + const pugi::char_t* iname = i->name; + if (iname && pugi::impl::strequal(name_, iname) && (PUGI_IMPL_NODETYPE(i) == type_)) + return XMLNode(i); + } + } + return XMLNode(); // if no node matching type_ was found: return an empty node + } + +} // namespace OpenXLSX + diff --git a/OpenXLSX/sources/utilities/XLUtilities.hpp b/OpenXLSX/sources/utilities/XLUtilities.hpp index 117f5ea..2b51d4d 100644 --- a/OpenXLSX/sources/utilities/XLUtilities.hpp +++ b/OpenXLSX/sources/utilities/XLUtilities.hpp @@ -9,6 +9,7 @@ #include #include // 2024-04-25 needed for xml_node_type_string +#include "XLConstants.hpp" // 2024-05-28 OpenXLSX::MAX_ROWS #include "XLCellReference.hpp" #include "XLCellValue.hpp" // OpenXLSX::XLValueType #include "XLContentTypes.hpp" // OpenXLSX::XLContentType @@ -97,6 +98,10 @@ namespace OpenXLSX */ inline XMLNode getRowNode(XMLNode sheetDataNode, uint32_t rowNumber) { + if( rowNumber > OpenXLSX::MAX_ROWS ) { // 2024-05-28: added range check + using namespace std::literals::string_literals; + throw XLCellAddressError( "rowNumber "s + std::to_string( rowNumber ) + " is outside valid range" ); + } // ===== Get the last child of sheetDataNode that is of type node_element. auto result = XMLNode(); result = sheetDataNode.last_child_of_type(pugi::node_element); @@ -111,7 +116,7 @@ namespace OpenXLSX // ===== If the requested node is closest to the end, start from the end and search backwards. else if (result.attribute("r").as_ullong() - rowNumber < rowNumber) { - while (!result.empty() && (result.attribute("r").as_ullong() > rowNumber)) result = result.previous_sibling_of_type(pugi::node_element); + while (not result.empty() && (result.attribute("r").as_ullong() > rowNumber)) result = result.previous_sibling_of_type(pugi::node_element); // ===== If the backwards search failed to locate the requested row if (result.empty() || (result.attribute("r").as_ullong() != rowNumber)) { if (result.empty()) @@ -155,6 +160,8 @@ namespace OpenXLSX inline XMLNode getCellNode(XMLNode rowNode, uint16_t columnNumber, uint32_t rowNumber = 0 ) { auto cellNode = XMLNode(); + if( rowNode.empty() ) return cellNode; // 2024-05-28: return an empty node in case of empty rowNode + cellNode = rowNode.last_child_of_type(pugi::node_element); if( !rowNumber ) rowNumber = rowNode.attribute("r").as_uint(); // if not provided, determine from rowNode auto cellRef = XLCellReference(rowNumber, columnNumber); @@ -167,7 +174,8 @@ namespace OpenXLSX } // ===== If the requested node is closest to the end, start from the end and search backwards... else if (XLCellReference(cellNode.attribute("r").value()).column() - columnNumber < columnNumber) { - while (!cellNode.empty() && (XLCellReference(cellNode.attribute("r").value()).column() > columnNumber)) cellNode = cellNode.previous_sibling_of_type(pugi::node_element); + while (not cellNode.empty() && (XLCellReference(cellNode.attribute("r").value()).column() > columnNumber)) + cellNode = cellNode.previous_sibling_of_type(pugi::node_element); // ===== If the backwards search failed to locate the requested cell if (cellNode.empty() || (XLCellReference(cellNode.attribute("r").value()).column() < columnNumber)) { if (cellNode.empty()) // If between row begin and higher column number, only non-element nodes exist @@ -183,7 +191,8 @@ namespace OpenXLSX cellNode = rowNode.first_child_of_type(pugi::node_element); // ===== It has been verified above that the requested columnNumber is <= the column number of the last node_element, therefore this loop will halt: - while (XLCellReference(cellNode.attribute("r").value()).column() < columnNumber) cellNode = cellNode.next_sibling_of_type(pugi::node_element); + while (XLCellReference(cellNode.attribute("r").value()).column() < columnNumber) + cellNode = cellNode.next_sibling_of_type(pugi::node_element); // ===== If the forwards search failed to locate the requested cell if (XLCellReference(cellNode.attribute("r").value()).column() > columnNumber) { cellNode = rowNode.insert_child_before("c", cellNode); diff --git a/cmake-cleanup.sh b/cmake-cleanup.sh new file mode 100755 index 0000000..b29f34e --- /dev/null +++ b/cmake-cleanup.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +# save original internal field separator & set it to newline +DEFAULT_IFS=$IFS +IFS=$'\n' + +if [ "$1" = "doit" ]; then + # delete individual known files, suppressing error output (in case they do not exist) + file="CMakeCache.txt" + recurse="" + if [ -e "$file" ]; then + rm $recurse "$file" + fi + file="cmake-log" + recurse="" + if [ -e "$file" ]; then + rm $recurse "$file" + fi + file="OpenXLSX/OpenXLSX/" + recurse="-r" + if [ -e "$file" ]; then + rm $recurse "$file" + fi + + # use find command to locate files and folders that occur multiple times & then loop to delete them + CMakeFiles=`find . -name "CMakeFiles" -exec echo {} \;` + for file in $CMakeFiles; do + rm -r "$file" + done + cmake_install_cmake=`find . -name "cmake_install.cmake" -exec echo {} \;` + for file in $cmake_install_cmake; do + rm -r "$file" + done + Makefiles=`find . -name "Makefile" -exec echo {} \;` + for file in $Makefiles; do + rm -r "$file" + done +else + # echo commands for deleting individual known files + file="CMakeCache.txt" + recurse="" + if [ -e "$file" ]; then + echo "rm $recurse \"$file\"" + fi + file="cmake-log" + recurse="" + if [ -e "$file" ]; then + echo "rm $recurse \"$file\"" + fi + file="OpenXLSX/OpenXLSX/" + recurse="-r" + if [ -e "$file" ]; then + echo "rm $recurse \"$file\"" + fi + + # use find command to locate files and folders that occur multiple times & then echo commands for deleting them + CMakeFiles=`find . -name "CMakeFiles" -exec echo {} \;` + for file in $CMakeFiles; do + echo "rm -r \"$file\"" + done + cmake_install_cmake=`find . -name "cmake_install.cmake" -exec echo {} \;` + for file in $cmake_install_cmake; do + echo "rm -r \"$file\"" + done + Makefiles=`find . -name "Makefile" -exec echo {} \;` + for file in $Makefiles; do + echo "rm -r \"$file\"" + done +fi + +# restore original internal field separator +IFS=$DEFAULT_IFS diff --git a/gnu-make-crutch/OpenXLSX-Exports.hpp b/gnu-make-crutch/OpenXLSX-Exports.hpp new file mode 100644 index 0000000..aa562fb --- /dev/null +++ b/gnu-make-crutch/OpenXLSX-Exports.hpp @@ -0,0 +1,42 @@ + +#ifndef OPENXLSX_EXPORT_H +#define OPENXLSX_EXPORT_H + +#ifdef OPENXLSX_STATIC_DEFINE +# define OPENXLSX_EXPORT +# define OPENXLSX_HIDDEN +#else +# ifndef OPENXLSX_EXPORT +# ifdef OpenXLSX_EXPORTS + /* We are building this library */ +# define OPENXLSX_EXPORT +# else + /* We are using this library */ +# define OPENXLSX_EXPORT +# endif +# endif + +# ifndef OPENXLSX_HIDDEN +# define OPENXLSX_HIDDEN +# endif +#endif + +#ifndef OPENXLSX_DEPRECATED +# define OPENXLSX_DEPRECATED __attribute__ ((__deprecated__)) +#endif + +#ifndef OPENXLSX_DEPRECATED_EXPORT +# define OPENXLSX_DEPRECATED_EXPORT OPENXLSX_EXPORT OPENXLSX_DEPRECATED +#endif + +#ifndef OPENXLSX_DEPRECATED_NO_EXPORT +# define OPENXLSX_DEPRECATED_NO_EXPORT OPENXLSX_HIDDEN OPENXLSX_DEPRECATED +#endif + +#if 0 /* DEFINE_NO_DEPRECATED */ +# ifndef OPENXLSX_NO_DEPRECATED +# define OPENXLSX_NO_DEPRECATED +# endif +#endif + +#endif /* OPENXLSX_EXPORT_H */ diff --git a/make-gnu.sh b/make-gnu.sh new file mode 100755 index 0000000..51bc440 --- /dev/null +++ b/make-gnu.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +# assemble all arguments with individual quote pairs to echo correctly with string preservation +ARGS= +for arg in "$@" +do + ARGS="$ARGS \"$arg\"" +done + +echo "make --makefile Makefile.GNU $ARGS" +make --makefile Makefile.GNU "$@"