From 0024c2a897427be1eab41ecf5da1b72dca1accc6 Mon Sep 17 00:00:00 2001 From: KemoNine Date: Thu, 25 Aug 2022 08:41:17 -0400 Subject: [PATCH] add xeft ; unused config -- kept for posterity and safety while evaluating search options --- .gitignore | 2 + org/init.el | 9 + org/xeft/Makefile | 23 + org/xeft/README.md | 84 +++ org/xeft/gitignore | 2 + org/xeft/module/emacs-module-prelude.h | 164 ++++++ org/xeft/module/emacs-module.h | 763 +++++++++++++++++++++++++ org/xeft/module/xapian-lite-internal.h | 40 ++ org/xeft/module/xapian-lite.cc | 446 +++++++++++++++ org/xeft/xapian-lite.dll | Bin 0 -> 249430 bytes org/xeft/xeft.el | 760 ++++++++++++++++++++++++ 11 files changed, 2293 insertions(+) create mode 100644 org/xeft/Makefile create mode 100644 org/xeft/README.md create mode 100644 org/xeft/gitignore create mode 100644 org/xeft/module/emacs-module-prelude.h create mode 100644 org/xeft/module/emacs-module.h create mode 100644 org/xeft/module/xapian-lite-internal.h create mode 100644 org/xeft/module/xapian-lite.cc create mode 100644 org/xeft/xapian-lite.dll create mode 100644 org/xeft/xeft.el diff --git a/.gitignore b/.gitignore index ff3f263..c10f907 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.elc orig/ +org/deft **ido.last** **projectile-bookmarks** **/auto-save-list @@ -9,3 +10,4 @@ orig/ **/beancount-mode **/transient **/.org-id-locations +**/xapian-lite.so \ No newline at end of file diff --git a/org/init.el b/org/init.el index 0e834b1..6b8a391 100644 --- a/org/init.el +++ b/org/init.el @@ -80,6 +80,15 @@ (setq org-support-shift-select t) (setq org-src-fontify-natively t) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +; xeft search tool +; (add-to-list 'load-path "~/.emacs.d.profiles/org/xeft") +; (require 'xeft) +; (setq xeft-database "~/.emacs.d.profiles/org/deft") +; (setq xeft-directory "~/org/") +; (setq xeft-default-extension "org") +; (setq xeft-recursive t) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; tags (load "~/.emacs.d.profiles/org/config-org-tags") diff --git a/org/xeft/Makefile b/org/xeft/Makefile new file mode 100644 index 0000000..a57a749 --- /dev/null +++ b/org/xeft/Makefile @@ -0,0 +1,23 @@ +.POSIX: +# Even if this is unnecessary, it doesn’t hurt. +PREFIX=/usr/local +CXX=g++ +CXXFLAGS=-fPIC -I$(PREFIX)/include +LDFLAGS=-L$(PREFIX)/lib +LDLIBS=-lxapian + +# Dylib extensions. +ifeq ($(OS),Windows_NT) + SOEXT = dll +endif +ifeq ($(shell uname),Darwin) + SOEXT = dylib +else + SOEXT = so +endif + +xapian-lite.dll: module/xapian-lite.cc + $(CXX) $< -o $@ -static -municode -lWs2_32 $(CXXFLAGS) $(LDFLAGS) $(LDLIBS) + +clean: + rm -f *.so *.o diff --git a/org/xeft/README.md b/org/xeft/README.md new file mode 100644 index 0000000..357a355 --- /dev/null +++ b/org/xeft/README.md @@ -0,0 +1,84 @@ +![Demo gif](./demo.gif) + +# Usage + +To use Xeft the note searching interface, install it and type `M-x +xeft RET` to bring up the panel. If the dynamic module doesn’t already +exists, you are prompted to download or compile it automatically. If +you choose to download the module, no more action is required. If you +want to compile the module locally, refer to the next section for +prerequisites for compiling the module. + +Once the xeft buffer is up, type the search phrase in the first line. +Press `C-n` and `C-p` to go through each file. You can preview a file +in another window by pressing `SPC` on a file, or click the file with +the mouse. Press `RET` to open the file in the current window. + +Directory `xeft-directory` stores note files, directory +`xeft-database` stores the database. Xeft uses +`xeft-default-extension` to create new files, and it ignores files +with `xeft-ignore-extension`. + +By default, Xeft only searches for first level files in +`xeft-directory`, to make it search recursively, set `xeft-recursive` +to t. + +See the “xeft” customize group for more custom options and faces. + +# Queries + +On search queries: + +Since Xeft uses Xapian, it supports the query syntax Xapian supports: + +``` +AND, NOT, OR, XOR and parenthesizes ++word1 -word2 which matches documents that contains WORD1 but not + WORD2. +word1 NEAR word2 which matches documents in where word1 is near word2. +word1 ADJ word2 which matches documents in where word1 is near word2 + and word1 comes before word2 +"word1 word2" which matches exactly “word1 word2” +``` + +Xeft deviates from Xapian in one aspect: consecutive phrases have +implied `AND` between them. So `word1 word2 word3` is actually seen as +`word1 AND word2 AND word3`. + +See https://xapian.org/docs/queryparser.html for Xapian’s official +documentation on query syntax. + +# building the dynamic module + +To build the module, you need to have Xapian installed. On Mac, it can +be installed with macports by + +```shell +sudo port install xapian-core +``` + +Then, build the module by + +```shell +make PREFIX=/opt/local +``` + +Here `/opt/local` is the default prefix of macports, which is what I +used to install Xapian. Homebrew and Linux users probably can leave it +empty. + +I can’t test it but on windows you can get msys2 and +`mingw-w64-x86_64-xapian-core` and `make` should just work. Thanks to +pRot0ta1p for reporting this. + +# notdeft + +I owe many thanks to the author of notdeft. I don’t really know C++ or +Xapian, without reading his code I wouldn’t be able to write Xeft. + +Also, if you want a more powerful searching experience, you will be +happier using notdeft instead. + +# Xapian dynamic module + +I wrote a xapian dynamic module that you can use too. Check it out at . diff --git a/org/xeft/gitignore b/org/xeft/gitignore new file mode 100644 index 0000000..e2d2375 --- /dev/null +++ b/org/xeft/gitignore @@ -0,0 +1,2 @@ +*.so +*.o \ No newline at end of file diff --git a/org/xeft/module/emacs-module-prelude.h b/org/xeft/module/emacs-module-prelude.h new file mode 100644 index 0000000..4edb9d1 --- /dev/null +++ b/org/xeft/module/emacs-module-prelude.h @@ -0,0 +1,164 @@ +#include "emacs-module.h" +#include +#include +#include +#include + +#ifndef EMACS_MODULE_PRELUDE_H +#define EMACS_MODULE_PRELUDE_H + +#define EMP_MAJOR_VERSION 1 +#define EMP_MINOR_VERSION 0 +#define EMP_PATCH_VERSION 0 + + +/* + Copy a Lisp string VALUE into BUFFER, and store the string size in + SIZE. A user doesn’t need to allocate BUFFER, but it is the user’s + responsibility to free it. If failed, return false, and the buffer + doesn’t need to be freed. + */ +bool +emp_copy_string_contents +(emacs_env *env, emacs_value value, char **buffer, size_t *size) +/* Copied from Pillipp’s document. I commented out assertions. */ +{ + ptrdiff_t buffer_size; + if (!env->copy_string_contents (env, value, NULL, &buffer_size)) + return false; + /* assert (env->non_local_exit_check (env) == emacs_funcall_exit_return); */ + /* assert (buffer_size > 0); */ + *buffer = (char*) malloc ((size_t) buffer_size); + if (*buffer == NULL) + { + env->non_local_exit_signal (env, env->intern (env, "memory-full"), + env->intern (env, "nil")); + return false; + } + ptrdiff_t old_buffer_size = buffer_size; + if (!env->copy_string_contents (env, value, *buffer, &buffer_size)) + { + free (*buffer); + *buffer = NULL; + return false; + } + /* assert (env->non_local_exit_check (env) == emacs_funcall_exit_return); */ + /* assert (buffer_size == old_buffer_size); */ + *size = (size_t) (buffer_size - 1); + return true; +} + +/* + Return a Lisp string. This is basically env->make_string except that + it calls strlen for you. + */ +emacs_value +emp_build_string (emacs_env *env, const char *string) +{ + return env->make_string (env, string, strlen (string)); +} + +/* + Intern NAME to a symbol. NAME has to be all-ASCII. + */ +emacs_value +emp_intern (emacs_env *env, const char *name) +{ + return env->intern (env, name); +} + +/* + Call a function named FN which takes NARGS number of arguments. + Example: funcall (env, "cons", 2, car, cdr); + */ +emacs_value +emp_funcall (emacs_env *env, const char* fn, ptrdiff_t nargs, ...) +{ + va_list argv; + va_start (argv, nargs); + emacs_value *args = (emacs_value *) malloc(nargs * sizeof(emacs_value)); + for (int idx = 0; idx < nargs; idx++) + { + args[idx] = va_arg (argv, emacs_value); + } + va_end (argv); + emacs_value val = env->funcall (env, emp_intern (env, fn), nargs, args); + free (args); + return val; +} + +/* + Provide FEATURE like ‘provide’ in Lisp. +*/ +void +emp_provide (emacs_env *env, const char *feature) +{ + emp_funcall (env, "provide", 1, emp_intern (env, feature)); +} + +/* + Raise a signal where NAME is the signal name and MESSAGE is the + error message. + */ +void +emp_signal_message1 +(emacs_env *env, const char *name, const char *message) +{ + env->non_local_exit_signal + (env, env->intern (env, name), + emp_funcall (env, "cons", 2, + env->make_string (env, message, strlen (message)), + emp_intern (env, "nil"))); +} + +/* + Define an error like ‘define-error’. + */ +void +emp_define_error +(emacs_env *env, const char *name, + const char *description, const char *parent) +{ + emp_funcall (env, "define-error", 3, + emp_intern (env, name), + env->make_string (env, description, strlen (description)), + emp_intern (env, parent)); +} + +/* + Return true if VAL is symbol nil. + */ +bool +emp_nilp (emacs_env *env, emacs_value val) +{ + return !env->is_not_nil (env, val); +} + +/* + Define a function NAME. The number of arguments that the function + takes is between MIN_ARITY and MAX_ARITY. FUNCTION is a function + with signature + + static emacs_value + function + (emacs_env *env, ptrdiff_t nargs, emacs_value args[], void *data) + EMACS_NOEXCEPT + + DOCUMENTATION is the docstring for FUNCTION. + */ +void +emp_define_function +(emacs_env *env, const char *name, ptrdiff_t min_arity, + ptrdiff_t max_arity, + emacs_value (*function) (emacs_env *env, + ptrdiff_t nargs, + emacs_value* args, + void *data) EMACS_NOEXCEPT, + const char *documentation) +{ + emacs_value fn = env->make_function + (env, min_arity, max_arity, function, documentation, NULL); + emp_funcall (env, "fset", 2, emp_intern (env, name), fn); +} + +#endif /* EMACS_MODULE_PRELUDE_H */ diff --git a/org/xeft/module/emacs-module.h b/org/xeft/module/emacs-module.h new file mode 100644 index 0000000..1185c06 --- /dev/null +++ b/org/xeft/module/emacs-module.h @@ -0,0 +1,763 @@ +/* emacs-module.h - GNU Emacs module API. + +Copyright (C) 2015-2021 Free Software Foundation, Inc. + +This file is part of GNU Emacs. + +GNU Emacs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or (at +your option) any later version. + +GNU Emacs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Emacs. If not, see . */ + +/* +This file defines the Emacs module API. Please see the chapter +`Dynamic Modules' in the GNU Emacs Lisp Reference Manual for +information how to write modules and use this header file. +*/ + +#ifndef EMACS_MODULE_H +#define EMACS_MODULE_H + +#include +#include +#include + +#ifndef __cplusplus +#include +#endif + +#define EMACS_MAJOR_VERSION 28 + +#if defined __cplusplus && __cplusplus >= 201103L +# define EMACS_NOEXCEPT noexcept +#else +# define EMACS_NOEXCEPT +#endif + +#if defined __cplusplus && __cplusplus >= 201703L +# define EMACS_NOEXCEPT_TYPEDEF noexcept +#else +# define EMACS_NOEXCEPT_TYPEDEF +#endif + +#if 3 < __GNUC__ + (3 <= __GNUC_MINOR__) +# define EMACS_ATTRIBUTE_NONNULL(...) \ + __attribute__ ((__nonnull__ (__VA_ARGS__))) +#elif (defined __has_attribute \ + && (!defined __clang_minor__ \ + || 3 < __clang_major__ + (5 <= __clang_minor__))) +# if __has_attribute (__nonnull__) +# define EMACS_ATTRIBUTE_NONNULL(...) \ + __attribute__ ((__nonnull__ (__VA_ARGS__))) +# endif +#endif +#ifndef EMACS_ATTRIBUTE_NONNULL +# define EMACS_ATTRIBUTE_NONNULL(...) +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* Current environment. */ +typedef struct emacs_env_28 emacs_env; + +/* Opaque pointer representing an Emacs Lisp value. + BEWARE: Do not assume NULL is a valid value! */ +typedef struct emacs_value_tag *emacs_value; + +enum { emacs_variadic_function = -2 }; + +/* Struct passed to a module init function (emacs_module_init). */ +struct emacs_runtime +{ + /* Structure size (for version checking). */ + ptrdiff_t size; + + /* Private data; users should not touch this. */ + struct emacs_runtime_private *private_members; + + /* Return an environment pointer. */ + emacs_env *(*get_environment) (struct emacs_runtime *runtime) + EMACS_ATTRIBUTE_NONNULL (1); +}; + +/* Type aliases for function pointer types used in the module API. + Note that we don't use these aliases directly in the API to be able + to mark the function arguments as 'noexcept' before C++20. + However, users can use them if they want. */ + +/* Function prototype for the module Lisp functions. These must not + throw C++ exceptions. */ +typedef emacs_value (*emacs_function) (emacs_env *env, ptrdiff_t nargs, + emacs_value *args, + void *data) + EMACS_NOEXCEPT_TYPEDEF EMACS_ATTRIBUTE_NONNULL (1); + +/* Function prototype for module user-pointer and function finalizers. + These must not throw C++ exceptions. */ +typedef void (*emacs_finalizer) (void *data) EMACS_NOEXCEPT_TYPEDEF; + +/* Possible Emacs function call outcomes. */ +enum emacs_funcall_exit +{ + /* Function has returned normally. */ + emacs_funcall_exit_return = 0, + + /* Function has signaled an error using `signal'. */ + emacs_funcall_exit_signal = 1, + + /* Function has exit using `throw'. */ + emacs_funcall_exit_throw = 2 +}; + +/* Possible return values for emacs_env.process_input. */ +enum emacs_process_input_result +{ + /* Module code may continue */ + emacs_process_input_continue = 0, + + /* Module code should return control to Emacs as soon as possible. */ + emacs_process_input_quit = 1 +}; + +/* Define emacs_limb_t so that it is likely to match GMP's mp_limb_t. + This micro-optimization can help modules that use mpz_export and + mpz_import, which operate more efficiently on mp_limb_t. It's OK + (if perhaps a bit slower) if the two types do not match, and + modules shouldn't rely on the two types matching. */ +typedef size_t emacs_limb_t; +#define EMACS_LIMB_MAX SIZE_MAX + +struct emacs_env_25 +{ + /* Structure size (for version checking). */ + ptrdiff_t size; + + /* Private data; users should not touch this. */ + struct emacs_env_private *private_members; + + /* Memory management. */ + + emacs_value (*make_global_ref) (emacs_env *env, emacs_value value) + EMACS_ATTRIBUTE_NONNULL(1); + + void (*free_global_ref) (emacs_env *env, emacs_value global_value) + EMACS_ATTRIBUTE_NONNULL(1); + + /* Non-local exit handling. */ + + enum emacs_funcall_exit (*non_local_exit_check) (emacs_env *env) + EMACS_ATTRIBUTE_NONNULL(1); + + void (*non_local_exit_clear) (emacs_env *env) + EMACS_ATTRIBUTE_NONNULL(1); + + enum emacs_funcall_exit (*non_local_exit_get) + (emacs_env *env, emacs_value *symbol, emacs_value *data) + EMACS_ATTRIBUTE_NONNULL(1, 2, 3); + + void (*non_local_exit_signal) (emacs_env *env, + emacs_value symbol, emacs_value data) + EMACS_ATTRIBUTE_NONNULL(1); + + void (*non_local_exit_throw) (emacs_env *env, + emacs_value tag, emacs_value value) + EMACS_ATTRIBUTE_NONNULL(1); + + /* Function registration. */ + + emacs_value (*make_function) (emacs_env *env, + ptrdiff_t min_arity, + ptrdiff_t max_arity, + emacs_value (*func) (emacs_env *env, + ptrdiff_t nargs, + emacs_value* args, + void *data) + EMACS_NOEXCEPT + EMACS_ATTRIBUTE_NONNULL(1), + const char *docstring, + void *data) + EMACS_ATTRIBUTE_NONNULL(1, 4); + + emacs_value (*funcall) (emacs_env *env, + emacs_value func, + ptrdiff_t nargs, + emacs_value* args) + EMACS_ATTRIBUTE_NONNULL(1); + + emacs_value (*intern) (emacs_env *env, const char *name) + EMACS_ATTRIBUTE_NONNULL(1, 2); + + /* Type conversion. */ + + emacs_value (*type_of) (emacs_env *env, emacs_value arg) + EMACS_ATTRIBUTE_NONNULL(1); + + bool (*is_not_nil) (emacs_env *env, emacs_value arg) + EMACS_ATTRIBUTE_NONNULL(1); + + bool (*eq) (emacs_env *env, emacs_value a, emacs_value b) + EMACS_ATTRIBUTE_NONNULL(1); + + intmax_t (*extract_integer) (emacs_env *env, emacs_value arg) + EMACS_ATTRIBUTE_NONNULL(1); + + emacs_value (*make_integer) (emacs_env *env, intmax_t n) + EMACS_ATTRIBUTE_NONNULL(1); + + double (*extract_float) (emacs_env *env, emacs_value arg) + EMACS_ATTRIBUTE_NONNULL(1); + + emacs_value (*make_float) (emacs_env *env, double d) + EMACS_ATTRIBUTE_NONNULL(1); + + /* Copy the content of the Lisp string VALUE to BUFFER as an utf8 + null-terminated string. + + SIZE must point to the total size of the buffer. If BUFFER is + NULL or if SIZE is not big enough, write the required buffer size + to SIZE and return true. + + Note that SIZE must include the last null byte (e.g. "abc" needs + a buffer of size 4). + + Return true if the string was successfully copied. */ + + bool (*copy_string_contents) (emacs_env *env, + emacs_value value, + char *buf, + ptrdiff_t *len) + EMACS_ATTRIBUTE_NONNULL(1, 4); + + /* Create a Lisp string from a utf8 encoded string. */ + emacs_value (*make_string) (emacs_env *env, + const char *str, ptrdiff_t len) + EMACS_ATTRIBUTE_NONNULL(1, 2); + + /* Embedded pointer type. */ + emacs_value (*make_user_ptr) (emacs_env *env, + void (*fin) (void *) EMACS_NOEXCEPT, + void *ptr) + EMACS_ATTRIBUTE_NONNULL(1); + + void *(*get_user_ptr) (emacs_env *env, emacs_value arg) + EMACS_ATTRIBUTE_NONNULL(1); + void (*set_user_ptr) (emacs_env *env, emacs_value arg, void *ptr) + EMACS_ATTRIBUTE_NONNULL(1); + + void (*(*get_user_finalizer) (emacs_env *env, emacs_value uptr)) + (void *) EMACS_NOEXCEPT EMACS_ATTRIBUTE_NONNULL(1); + void (*set_user_finalizer) (emacs_env *env, emacs_value arg, + void (*fin) (void *) EMACS_NOEXCEPT) + EMACS_ATTRIBUTE_NONNULL(1); + + /* Vector functions. */ + emacs_value (*vec_get) (emacs_env *env, emacs_value vector, ptrdiff_t index) + EMACS_ATTRIBUTE_NONNULL(1); + + void (*vec_set) (emacs_env *env, emacs_value vector, ptrdiff_t index, + emacs_value value) + EMACS_ATTRIBUTE_NONNULL(1); + + ptrdiff_t (*vec_size) (emacs_env *env, emacs_value vector) + EMACS_ATTRIBUTE_NONNULL(1); +}; + +struct emacs_env_26 +{ + /* Structure size (for version checking). */ + ptrdiff_t size; + + /* Private data; users should not touch this. */ + struct emacs_env_private *private_members; + + /* Memory management. */ + + emacs_value (*make_global_ref) (emacs_env *env, emacs_value value) + EMACS_ATTRIBUTE_NONNULL(1); + + void (*free_global_ref) (emacs_env *env, emacs_value global_value) + EMACS_ATTRIBUTE_NONNULL(1); + + /* Non-local exit handling. */ + + enum emacs_funcall_exit (*non_local_exit_check) (emacs_env *env) + EMACS_ATTRIBUTE_NONNULL(1); + + void (*non_local_exit_clear) (emacs_env *env) + EMACS_ATTRIBUTE_NONNULL(1); + + enum emacs_funcall_exit (*non_local_exit_get) + (emacs_env *env, emacs_value *symbol, emacs_value *data) + EMACS_ATTRIBUTE_NONNULL(1, 2, 3); + + void (*non_local_exit_signal) (emacs_env *env, + emacs_value symbol, emacs_value data) + EMACS_ATTRIBUTE_NONNULL(1); + + void (*non_local_exit_throw) (emacs_env *env, + emacs_value tag, emacs_value value) + EMACS_ATTRIBUTE_NONNULL(1); + + /* Function registration. */ + + emacs_value (*make_function) (emacs_env *env, + ptrdiff_t min_arity, + ptrdiff_t max_arity, + emacs_value (*func) (emacs_env *env, + ptrdiff_t nargs, + emacs_value* args, + void *data) + EMACS_NOEXCEPT + EMACS_ATTRIBUTE_NONNULL(1), + const char *docstring, + void *data) + EMACS_ATTRIBUTE_NONNULL(1, 4); + + emacs_value (*funcall) (emacs_env *env, + emacs_value func, + ptrdiff_t nargs, + emacs_value* args) + EMACS_ATTRIBUTE_NONNULL(1); + + emacs_value (*intern) (emacs_env *env, const char *name) + EMACS_ATTRIBUTE_NONNULL(1, 2); + + /* Type conversion. */ + + emacs_value (*type_of) (emacs_env *env, emacs_value arg) + EMACS_ATTRIBUTE_NONNULL(1); + + bool (*is_not_nil) (emacs_env *env, emacs_value arg) + EMACS_ATTRIBUTE_NONNULL(1); + + bool (*eq) (emacs_env *env, emacs_value a, emacs_value b) + EMACS_ATTRIBUTE_NONNULL(1); + + intmax_t (*extract_integer) (emacs_env *env, emacs_value arg) + EMACS_ATTRIBUTE_NONNULL(1); + + emacs_value (*make_integer) (emacs_env *env, intmax_t n) + EMACS_ATTRIBUTE_NONNULL(1); + + double (*extract_float) (emacs_env *env, emacs_value arg) + EMACS_ATTRIBUTE_NONNULL(1); + + emacs_value (*make_float) (emacs_env *env, double d) + EMACS_ATTRIBUTE_NONNULL(1); + + /* Copy the content of the Lisp string VALUE to BUFFER as an utf8 + null-terminated string. + + SIZE must point to the total size of the buffer. If BUFFER is + NULL or if SIZE is not big enough, write the required buffer size + to SIZE and return true. + + Note that SIZE must include the last null byte (e.g. "abc" needs + a buffer of size 4). + + Return true if the string was successfully copied. */ + + bool (*copy_string_contents) (emacs_env *env, + emacs_value value, + char *buf, + ptrdiff_t *len) + EMACS_ATTRIBUTE_NONNULL(1, 4); + + /* Create a Lisp string from a utf8 encoded string. */ + emacs_value (*make_string) (emacs_env *env, + const char *str, ptrdiff_t len) + EMACS_ATTRIBUTE_NONNULL(1, 2); + + /* Embedded pointer type. */ + emacs_value (*make_user_ptr) (emacs_env *env, + void (*fin) (void *) EMACS_NOEXCEPT, + void *ptr) + EMACS_ATTRIBUTE_NONNULL(1); + + void *(*get_user_ptr) (emacs_env *env, emacs_value arg) + EMACS_ATTRIBUTE_NONNULL(1); + void (*set_user_ptr) (emacs_env *env, emacs_value arg, void *ptr) + EMACS_ATTRIBUTE_NONNULL(1); + + void (*(*get_user_finalizer) (emacs_env *env, emacs_value uptr)) + (void *) EMACS_NOEXCEPT EMACS_ATTRIBUTE_NONNULL(1); + void (*set_user_finalizer) (emacs_env *env, emacs_value arg, + void (*fin) (void *) EMACS_NOEXCEPT) + EMACS_ATTRIBUTE_NONNULL(1); + + /* Vector functions. */ + emacs_value (*vec_get) (emacs_env *env, emacs_value vector, ptrdiff_t index) + EMACS_ATTRIBUTE_NONNULL(1); + + void (*vec_set) (emacs_env *env, emacs_value vector, ptrdiff_t index, + emacs_value value) + EMACS_ATTRIBUTE_NONNULL(1); + + ptrdiff_t (*vec_size) (emacs_env *env, emacs_value vector) + EMACS_ATTRIBUTE_NONNULL(1); + + /* Returns whether a quit is pending. */ + bool (*should_quit) (emacs_env *env) + EMACS_ATTRIBUTE_NONNULL(1); +}; + +struct emacs_env_27 +{ + /* Structure size (for version checking). */ + ptrdiff_t size; + + /* Private data; users should not touch this. */ + struct emacs_env_private *private_members; + + /* Memory management. */ + + emacs_value (*make_global_ref) (emacs_env *env, emacs_value value) + EMACS_ATTRIBUTE_NONNULL(1); + + void (*free_global_ref) (emacs_env *env, emacs_value global_value) + EMACS_ATTRIBUTE_NONNULL(1); + + /* Non-local exit handling. */ + + enum emacs_funcall_exit (*non_local_exit_check) (emacs_env *env) + EMACS_ATTRIBUTE_NONNULL(1); + + void (*non_local_exit_clear) (emacs_env *env) + EMACS_ATTRIBUTE_NONNULL(1); + + enum emacs_funcall_exit (*non_local_exit_get) + (emacs_env *env, emacs_value *symbol, emacs_value *data) + EMACS_ATTRIBUTE_NONNULL(1, 2, 3); + + void (*non_local_exit_signal) (emacs_env *env, + emacs_value symbol, emacs_value data) + EMACS_ATTRIBUTE_NONNULL(1); + + void (*non_local_exit_throw) (emacs_env *env, + emacs_value tag, emacs_value value) + EMACS_ATTRIBUTE_NONNULL(1); + + /* Function registration. */ + + emacs_value (*make_function) (emacs_env *env, + ptrdiff_t min_arity, + ptrdiff_t max_arity, + emacs_value (*func) (emacs_env *env, + ptrdiff_t nargs, + emacs_value* args, + void *data) + EMACS_NOEXCEPT + EMACS_ATTRIBUTE_NONNULL(1), + const char *docstring, + void *data) + EMACS_ATTRIBUTE_NONNULL(1, 4); + + emacs_value (*funcall) (emacs_env *env, + emacs_value func, + ptrdiff_t nargs, + emacs_value* args) + EMACS_ATTRIBUTE_NONNULL(1); + + emacs_value (*intern) (emacs_env *env, const char *name) + EMACS_ATTRIBUTE_NONNULL(1, 2); + + /* Type conversion. */ + + emacs_value (*type_of) (emacs_env *env, emacs_value arg) + EMACS_ATTRIBUTE_NONNULL(1); + + bool (*is_not_nil) (emacs_env *env, emacs_value arg) + EMACS_ATTRIBUTE_NONNULL(1); + + bool (*eq) (emacs_env *env, emacs_value a, emacs_value b) + EMACS_ATTRIBUTE_NONNULL(1); + + intmax_t (*extract_integer) (emacs_env *env, emacs_value arg) + EMACS_ATTRIBUTE_NONNULL(1); + + emacs_value (*make_integer) (emacs_env *env, intmax_t n) + EMACS_ATTRIBUTE_NONNULL(1); + + double (*extract_float) (emacs_env *env, emacs_value arg) + EMACS_ATTRIBUTE_NONNULL(1); + + emacs_value (*make_float) (emacs_env *env, double d) + EMACS_ATTRIBUTE_NONNULL(1); + + /* Copy the content of the Lisp string VALUE to BUFFER as an utf8 + null-terminated string. + + SIZE must point to the total size of the buffer. If BUFFER is + NULL or if SIZE is not big enough, write the required buffer size + to SIZE and return true. + + Note that SIZE must include the last null byte (e.g. "abc" needs + a buffer of size 4). + + Return true if the string was successfully copied. */ + + bool (*copy_string_contents) (emacs_env *env, + emacs_value value, + char *buf, + ptrdiff_t *len) + EMACS_ATTRIBUTE_NONNULL(1, 4); + + /* Create a Lisp string from a utf8 encoded string. */ + emacs_value (*make_string) (emacs_env *env, + const char *str, ptrdiff_t len) + EMACS_ATTRIBUTE_NONNULL(1, 2); + + /* Embedded pointer type. */ + emacs_value (*make_user_ptr) (emacs_env *env, + void (*fin) (void *) EMACS_NOEXCEPT, + void *ptr) + EMACS_ATTRIBUTE_NONNULL(1); + + void *(*get_user_ptr) (emacs_env *env, emacs_value arg) + EMACS_ATTRIBUTE_NONNULL(1); + void (*set_user_ptr) (emacs_env *env, emacs_value arg, void *ptr) + EMACS_ATTRIBUTE_NONNULL(1); + + void (*(*get_user_finalizer) (emacs_env *env, emacs_value uptr)) + (void *) EMACS_NOEXCEPT EMACS_ATTRIBUTE_NONNULL(1); + void (*set_user_finalizer) (emacs_env *env, emacs_value arg, + void (*fin) (void *) EMACS_NOEXCEPT) + EMACS_ATTRIBUTE_NONNULL(1); + + /* Vector functions. */ + emacs_value (*vec_get) (emacs_env *env, emacs_value vector, ptrdiff_t index) + EMACS_ATTRIBUTE_NONNULL(1); + + void (*vec_set) (emacs_env *env, emacs_value vector, ptrdiff_t index, + emacs_value value) + EMACS_ATTRIBUTE_NONNULL(1); + + ptrdiff_t (*vec_size) (emacs_env *env, emacs_value vector) + EMACS_ATTRIBUTE_NONNULL(1); + + /* Returns whether a quit is pending. */ + bool (*should_quit) (emacs_env *env) + EMACS_ATTRIBUTE_NONNULL(1); + + /* Processes pending input events and returns whether the module + function should quit. */ + enum emacs_process_input_result (*process_input) (emacs_env *env) + EMACS_ATTRIBUTE_NONNULL (1); + + struct timespec (*extract_time) (emacs_env *env, emacs_value arg) + EMACS_ATTRIBUTE_NONNULL (1); + + emacs_value (*make_time) (emacs_env *env, struct timespec time) + EMACS_ATTRIBUTE_NONNULL (1); + + bool (*extract_big_integer) (emacs_env *env, emacs_value arg, int *sign, + ptrdiff_t *count, emacs_limb_t *magnitude) + EMACS_ATTRIBUTE_NONNULL (1); + + emacs_value (*make_big_integer) (emacs_env *env, int sign, ptrdiff_t count, + const emacs_limb_t *magnitude) + EMACS_ATTRIBUTE_NONNULL (1); +}; + +struct emacs_env_28 +{ + /* Structure size (for version checking). */ + ptrdiff_t size; + + /* Private data; users should not touch this. */ + struct emacs_env_private *private_members; + + /* Memory management. */ + + emacs_value (*make_global_ref) (emacs_env *env, emacs_value value) + EMACS_ATTRIBUTE_NONNULL(1); + + void (*free_global_ref) (emacs_env *env, emacs_value global_value) + EMACS_ATTRIBUTE_NONNULL(1); + + /* Non-local exit handling. */ + + enum emacs_funcall_exit (*non_local_exit_check) (emacs_env *env) + EMACS_ATTRIBUTE_NONNULL(1); + + void (*non_local_exit_clear) (emacs_env *env) + EMACS_ATTRIBUTE_NONNULL(1); + + enum emacs_funcall_exit (*non_local_exit_get) + (emacs_env *env, emacs_value *symbol, emacs_value *data) + EMACS_ATTRIBUTE_NONNULL(1, 2, 3); + + void (*non_local_exit_signal) (emacs_env *env, + emacs_value symbol, emacs_value data) + EMACS_ATTRIBUTE_NONNULL(1); + + void (*non_local_exit_throw) (emacs_env *env, + emacs_value tag, emacs_value value) + EMACS_ATTRIBUTE_NONNULL(1); + + /* Function registration. */ + + emacs_value (*make_function) (emacs_env *env, + ptrdiff_t min_arity, + ptrdiff_t max_arity, + emacs_value (*func) (emacs_env *env, + ptrdiff_t nargs, + emacs_value* args, + void *data) + EMACS_NOEXCEPT + EMACS_ATTRIBUTE_NONNULL(1), + const char *docstring, + void *data) + EMACS_ATTRIBUTE_NONNULL(1, 4); + + emacs_value (*funcall) (emacs_env *env, + emacs_value func, + ptrdiff_t nargs, + emacs_value* args) + EMACS_ATTRIBUTE_NONNULL(1); + + emacs_value (*intern) (emacs_env *env, const char *name) + EMACS_ATTRIBUTE_NONNULL(1, 2); + + /* Type conversion. */ + + emacs_value (*type_of) (emacs_env *env, emacs_value arg) + EMACS_ATTRIBUTE_NONNULL(1); + + bool (*is_not_nil) (emacs_env *env, emacs_value arg) + EMACS_ATTRIBUTE_NONNULL(1); + + bool (*eq) (emacs_env *env, emacs_value a, emacs_value b) + EMACS_ATTRIBUTE_NONNULL(1); + + intmax_t (*extract_integer) (emacs_env *env, emacs_value arg) + EMACS_ATTRIBUTE_NONNULL(1); + + emacs_value (*make_integer) (emacs_env *env, intmax_t n) + EMACS_ATTRIBUTE_NONNULL(1); + + double (*extract_float) (emacs_env *env, emacs_value arg) + EMACS_ATTRIBUTE_NONNULL(1); + + emacs_value (*make_float) (emacs_env *env, double d) + EMACS_ATTRIBUTE_NONNULL(1); + + /* Copy the content of the Lisp string VALUE to BUFFER as an utf8 + null-terminated string. + + SIZE must point to the total size of the buffer. If BUFFER is + NULL or if SIZE is not big enough, write the required buffer size + to SIZE and return true. + + Note that SIZE must include the last null byte (e.g. "abc" needs + a buffer of size 4). + + Return true if the string was successfully copied. */ + + bool (*copy_string_contents) (emacs_env *env, + emacs_value value, + char *buf, + ptrdiff_t *len) + EMACS_ATTRIBUTE_NONNULL(1, 4); + + /* Create a Lisp string from a utf8 encoded string. */ + emacs_value (*make_string) (emacs_env *env, + const char *str, ptrdiff_t len) + EMACS_ATTRIBUTE_NONNULL(1, 2); + + /* Embedded pointer type. */ + emacs_value (*make_user_ptr) (emacs_env *env, + void (*fin) (void *) EMACS_NOEXCEPT, + void *ptr) + EMACS_ATTRIBUTE_NONNULL(1); + + void *(*get_user_ptr) (emacs_env *env, emacs_value arg) + EMACS_ATTRIBUTE_NONNULL(1); + void (*set_user_ptr) (emacs_env *env, emacs_value arg, void *ptr) + EMACS_ATTRIBUTE_NONNULL(1); + + void (*(*get_user_finalizer) (emacs_env *env, emacs_value uptr)) + (void *) EMACS_NOEXCEPT EMACS_ATTRIBUTE_NONNULL(1); + void (*set_user_finalizer) (emacs_env *env, emacs_value arg, + void (*fin) (void *) EMACS_NOEXCEPT) + EMACS_ATTRIBUTE_NONNULL(1); + + /* Vector functions. */ + emacs_value (*vec_get) (emacs_env *env, emacs_value vector, ptrdiff_t index) + EMACS_ATTRIBUTE_NONNULL(1); + + void (*vec_set) (emacs_env *env, emacs_value vector, ptrdiff_t index, + emacs_value value) + EMACS_ATTRIBUTE_NONNULL(1); + + ptrdiff_t (*vec_size) (emacs_env *env, emacs_value vector) + EMACS_ATTRIBUTE_NONNULL(1); + + /* Returns whether a quit is pending. */ + bool (*should_quit) (emacs_env *env) + EMACS_ATTRIBUTE_NONNULL(1); + + /* Processes pending input events and returns whether the module + function should quit. */ + enum emacs_process_input_result (*process_input) (emacs_env *env) + EMACS_ATTRIBUTE_NONNULL (1); + + struct timespec (*extract_time) (emacs_env *env, emacs_value arg) + EMACS_ATTRIBUTE_NONNULL (1); + + emacs_value (*make_time) (emacs_env *env, struct timespec time) + EMACS_ATTRIBUTE_NONNULL (1); + + bool (*extract_big_integer) (emacs_env *env, emacs_value arg, int *sign, + ptrdiff_t *count, emacs_limb_t *magnitude) + EMACS_ATTRIBUTE_NONNULL (1); + + emacs_value (*make_big_integer) (emacs_env *env, int sign, ptrdiff_t count, + const emacs_limb_t *magnitude) + EMACS_ATTRIBUTE_NONNULL (1); + + /* Add module environment functions newly added in Emacs 28 here. + Before Emacs 28 is released, remove this comment and start + module-env-29.h on the master branch. */ + + void (*(*EMACS_ATTRIBUTE_NONNULL (1) + get_function_finalizer) (emacs_env *env, + emacs_value arg)) (void *) EMACS_NOEXCEPT; + + void (*set_function_finalizer) (emacs_env *env, emacs_value arg, + void (*fin) (void *) EMACS_NOEXCEPT) + EMACS_ATTRIBUTE_NONNULL (1); + + int (*open_channel) (emacs_env *env, emacs_value pipe_process) + EMACS_ATTRIBUTE_NONNULL (1); + + void (*make_interactive) (emacs_env *env, emacs_value function, + emacs_value spec) + EMACS_ATTRIBUTE_NONNULL (1); + + /* Create a unibyte Lisp string from a string. */ + emacs_value (*make_unibyte_string) (emacs_env *env, + const char *str, ptrdiff_t len) + EMACS_ATTRIBUTE_NONNULL(1, 2); +}; + +/* Every module should define a function as follows. */ +extern int emacs_module_init (struct emacs_runtime *runtime) + EMACS_NOEXCEPT + EMACS_ATTRIBUTE_NONNULL (1); + +#ifdef __cplusplus +} +#endif + +#endif /* EMACS_MODULE_H */ diff --git a/org/xeft/module/xapian-lite-internal.h b/org/xeft/module/xapian-lite-internal.h new file mode 100644 index 0000000..d739004 --- /dev/null +++ b/org/xeft/module/xapian-lite-internal.h @@ -0,0 +1,40 @@ +#ifndef XAPIAN_LITE_INTERNAL_H +#define XAPIAN_LITE_INTERNAL_H + +#include "emacs-module.h" + +typedef emacs_value (*emacs_subr) (emacs_env *env, + ptrdiff_t nargs, emacs_value *args, + void *data); +#ifdef __cplusplus +extern "C" { +#endif + +void +define_error +(emacs_env *env, const char *name, + const char *description, const char *parent); + +emacs_value +Fxapian_lite_reindex_file +(emacs_env *env, ptrdiff_t nargs, emacs_value args[], void *data) + EMACS_NOEXCEPT; + +emacs_value +Fxapian_lite_query_term +(emacs_env *env, ptrdiff_t nargs, emacs_value args[], void *data) + EMACS_NOEXCEPT; + +void +define_function +(emacs_env *env, const char *name, ptrdiff_t min_arity, + ptrdiff_t max_arity, emacs_subr function, const char *documentation); + +void +provide (emacs_env *env, const char *feature); + +#ifdef __cplusplus +} +#endif + +#endif /* XAPIAN_LITE_INTERNAL_H */ diff --git a/org/xeft/module/xapian-lite.cc b/org/xeft/module/xapian-lite.cc new file mode 100644 index 0000000..0ff505f --- /dev/null +++ b/org/xeft/module/xapian-lite.cc @@ -0,0 +1,446 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include "emacs-module.h" +#include "emacs-module-prelude.h" + +using namespace std; + +__declspec(dllexport) int plugin_is_GPL_compatible; + +#if defined __cplusplus && __cplusplus >= 201103L +# define EMACS_NOEXCEPT noexcept +#else +# define EMACS_NOEXCEPT +#endif + +#define CHECK_EXIT(env) \ + if (env->non_local_exit_check (env) \ + != emacs_funcall_exit_return) \ + { return NULL; } + +/* A few notes: The database we use, WritableDatabase, will not throw + DatabaseModifiedError, so we don’t need to handle that. For query, + we first try to parse it with special syntax enabled, i.e., with + AND, OR, +/-, etc. If that doesn’t parse, we’ll just parse it as + plain text. + + REF: https://lists.xapian.org/pipermail/xapian-discuss/2021-August/009906.html + */ + +/*** Xapian stuff */ + +static const Xapian::valueno DOC_MTIME = 0; +static const Xapian::valueno DOC_FILEPATH = 1; + +static Xapian::WritableDatabase database; +static string cached_dbpath = ""; + +class xapian_lite_cannot_open_file: public exception {}; + +// Reindex the file at PATH, using database at DBPATH. Throws +// cannot_open_file. Both path must be absolute. Normally only reindex +// if file has change since last index, if FORCE is true, always +// reindex. Return true if re-indexed, return false if didn’t. +// LANG is the language used by the stemmer. +// Possible langauges: +// https://xapian.org/docs/apidoc/html/classXapian_1_1Stem.html +static bool +reindex_file +(string path, string dbpath, string lang = "en", bool force = false) +{ + // Check for mtime. + struct stat st; + time_t file_mtime; + off_t file_size; + if (stat (path.c_str(), &st) == 0) + { + file_mtime = st.st_mtime; + file_size = st.st_size; + } + else + { + throw xapian_lite_cannot_open_file(); + } + + // Even though the document says that database object only carries a + // pointer to the actual object, it is still not cheap enough. By + // using this cache, we get much better performance when reindexing + // hundreds of files, which most are no-op because they hasn’t been + // modified. + if (dbpath != cached_dbpath) + { + database = Xapian::WritableDatabase + (dbpath, Xapian::DB_CREATE_OR_OPEN); + cached_dbpath = dbpath; + } + // Track doc with file path as "id". See + // https://getting-started-with-xapian.readthedocs.io/en/latest/practical_example/indexing/updating_the_database.html + string termID = 'Q' + path; + Xapian::PostingIterator it_begin = database.postlist_begin (termID); + Xapian::PostingIterator it_end = database.postlist_end (termID); + bool has_doc = it_begin != it_end; + time_t db_mtime; + if (has_doc) + { + // sortable_serialise is for double and we can’t really use it. + Xapian::Document db_doc = database.get_document(*it_begin); + db_mtime = (time_t) stoi (db_doc.get_value (DOC_MTIME)); + } + + // Need re-index. + if (!has_doc || (has_doc && db_mtime < file_mtime) || force) + { + // Get the file content. + // REF: https://stackoverflow.com/questions/2912520/read-file-contents-into-a-string-in-c + ifstream infile (path); + string content ((istreambuf_iterator(infile)), + (istreambuf_iterator())); + // Create the indexer. + Xapian::TermGenerator indexer; + Xapian::Stem stemmer (lang); + indexer.set_stemmer (stemmer); + indexer.set_stemming_strategy + (Xapian::TermGenerator::STEM_SOME); + // Support CJK. + indexer.set_flags (Xapian::TermGenerator::FLAG_CJK_NGRAM); + // Index file content. + Xapian::Document new_doc; + indexer.set_document (new_doc); + indexer.index_text (content); + // Set doc info. + new_doc.add_boolean_term (termID); + // We store the path in value, no need to use set_data. + new_doc.add_value (DOC_FILEPATH, path); + new_doc.add_value (DOC_MTIME, (string) to_string (file_mtime)); + database.replace_document (termID, new_doc); + return true; + } + else + { + return false; + } +} + +// Query TERM in the databse at DBPATH. OFFSET and PAGE_SIZE is for +// paging, see the docstring for the lisp function. If a file in the +// result doesn’t exist anymore, it is removed from the database. +// LANG is the language used by the stemmer. +// Possible langauges: +// https://xapian.org/docs/apidoc/html/classXapian_1_1Stem.html +static vector +query_term +(string term, string dbpath, int offset, int page_size, + string lang = "en") +{ + // See reindex_file for the reason for caching the database object. + if (dbpath != cached_dbpath) + { + database = Xapian::WritableDatabase + (dbpath, Xapian::DB_CREATE_OR_OPEN); + cached_dbpath = dbpath; + } + + Xapian::QueryParser parser; + Xapian::Stem stemmer (lang); + parser.set_stemmer (stemmer); + parser.set_stemming_strategy (Xapian::QueryParser::STEM_SOME); + // Partial match (FLAG_PARTIAL) needs the database to expand + // wildcards. + parser.set_database(database); + + Xapian::Query query; + try + { + query = parser.parse_query + // CJK_NGRAM is the flag for CJK support. PARTIAL makes + // interactive search more stable. DEFAULT enables AND OR and + // +/-. + (term, Xapian::QueryParser::FLAG_CJK_NGRAM + | Xapian::QueryParser::FLAG_PARTIAL + | Xapian::QueryParser::FLAG_DEFAULT); + } + // If the syntax is syntactically wrong, Xapian throws this error. + // Try again without enabling any special syntax. + catch (Xapian::QueryParserError &e) + { + query = parser.parse_query + (term, Xapian::QueryParser::FLAG_CJK_NGRAM + | Xapian::QueryParser::FLAG_PARTIAL); + } + + Xapian::Enquire enquire (database); + enquire.set_query (query); + + Xapian::MSet mset = enquire.get_mset (offset, page_size); + vector result (0); + for (Xapian::MSetIterator it = mset.begin(); it != mset.end(); it++) + { + Xapian::Document doc = it.get_document(); + string path = doc.get_value(DOC_FILEPATH); + // If the file doesn’t exists anymore, remove it. + struct stat st; + if (stat (path.c_str(), &st) == 0) + { + result.push_back (doc.get_value (DOC_FILEPATH)); + } + else + { + database.delete_document (doc.get_docid()); + } + } + return result; +} + +/*** Module definition */ + +static string +copy_string (emacs_env *env, emacs_value value) +{ + char* char_buffer; + size_t size; + if (emp_copy_string_contents (env, value, &char_buffer, &size)) + { + string str = (string) char_buffer; + free (char_buffer); + return str; + } + else + { + emp_signal_message1 (env, "xapian-lite-error", + "Error turning lisp string to C++ string"); + return ""; + } +} + +static bool +NILP (emacs_env *env, emacs_value val) +{ + return !env->is_not_nil (env, val); +} + +static const char* xapian_lite_reindex_file_doc = + "Refindex file at PATH with database at DBPATH\n" + "Both paths has to be absolute. Normally, this function only\n" + "reindex a file if it has been modified since last indexed,\n" + "but if FORCE is non-nil, this function will always reindex.\n" + "Return non-nil if actually reindexed the file, return nil if not.\n" + "\n" + "LANG is the language used by the indexer, it tells Xapian how to\n" + "reduce words to word stems, e.g., apples <-> apple.\n" + "A full list of possible languages can be found at\n" + "https://xapian.org/docs/apidoc/html/classXapian_1_1Stem.html.\n" + "By default, LANG is \"en\".\n" + "\n" + "(fn PATH DBPATH &optional LANG FORCE)"; + +static emacs_value +Fxapian_lite_reindex_file +(emacs_env *env, ptrdiff_t nargs, emacs_value args[], void *data) + EMACS_NOEXCEPT +{ + + // Decode arguments. + emacs_value lisp_path = args[0]; + emacs_value lisp_dbpath = args[1]; + + if (NILP (env, emp_funcall (env, "file-name-absolute-p", 1, lisp_path))) + { + emp_signal_message1 (env, "xapian-lite-file-error", + "PATH is not a absolute path"); + return NULL; + } + if (NILP (env, + emp_funcall (env, "file-name-absolute-p", 1, lisp_dbpath))) + { + emp_signal_message1 (env, "xapian-lite-file-error", + "DBPATH is not a absolute path"); + return NULL; + } + + // Expand "~" in the filename. + emacs_value lisp_args[] = {lisp_path}; + lisp_path = emp_funcall (env, "expand-file-name", 1, lisp_path); + lisp_dbpath = emp_funcall (env, "expand-file-name", 1, lisp_dbpath); + + emacs_value lisp_lang = nargs < 3 ? emp_intern (env, "nil") : args[2]; + emacs_value lisp_force = nargs < 4 ? emp_intern (env, "nil") : args[3]; + + string path = copy_string (env, lisp_path); + string dbpath = copy_string (env, lisp_dbpath); + bool force = !NILP (env, lisp_force); + CHECK_EXIT (env); + string lang = NILP (env, lisp_lang) ? + "en" : copy_string (env, lisp_lang); + CHECK_EXIT (env); + + // Do the work. + bool indexed; + try + { + indexed = reindex_file (path, dbpath, lang, force); + return indexed ? emp_intern (env, "t") : emp_intern (env, "nil"); + } + catch (xapian_lite_cannot_open_file &e) + { + emp_signal_message1 (env, "xapian-lite-file-error", + "Cannot open the file"); + return NULL; + } + catch (Xapian::DatabaseCorruptError &e) + { + emp_signal_message1 (env, "xapian-lite-database-corrupt-error", + e.get_description().c_str()); + return NULL; + } + catch (Xapian::DatabaseLockError &e) + { + emp_signal_message1 (env, "xapian-lite-database-lock-error", + e.get_description().c_str()); + return NULL; + } + catch (Xapian::Error &e) + { + emp_signal_message1 (env, "xapian-lite-lib-error", + e.get_description().c_str()); + return NULL; + } + catch (exception &e) + { + emp_signal_message1 (env, "xapian-lite-error", + "Something went wrong"); + return NULL; + } +} + +static const char *xapian_lite_query_term_doc = + "Query for TERM in database at DBPATH.\n" + "Paging is supported by OFFSET and PAGE-SIZE. OFFSET specifies page\n" + "start, and PAGE-SIZE the size. For example, if a page is 10 entries,\n" + "OFFSET and PAGE-SIZE would be first 0 and 10, then 10 and 10, and\n" + "so on.\n" + "\n" + "If a file in the result doesn't exist anymore, it is removed from\n" + "the database, and is not included in the return value.\n" + "\n" + "LANG is the language used by the indexer, it tells Xapian how to\n" + "reduce words to word stems, e.g., apples <-> apple.\n" + "A full list of possible languages can be found at\n" + "https://xapian.org/docs/apidoc/html/classXapian_1_1Stem.html.\n" + "By default, LANG is \"en\".\n" + "\n" + "TERM can use common Xapian syntax like AND, OR, and +/-.\n" + "Specifically, this function supports:\n" + "\n" + " Boolean operators: AND, OR, XOR, NOT\n" + " Parenthesized expression: ()\n" + " Love/hate terms: +/-\n" + " Exact match: \"\"\n" + "\n" + "If TERM contains syntactic errors, like \"a AND AND b\",\n" + "it is treated as a plain term.\n" + "\n" + "(fn TERM DBPATH OFFSET PAGE-SIZE &optional LANG)"; + +static emacs_value +Fxapian_lite_query_term +(emacs_env *env, ptrdiff_t nargs, emacs_value args[], void *data) + EMACS_NOEXCEPT +{ + // Decode arguments. + emacs_value lisp_term = args[0]; + emacs_value lisp_dbpath = args[1]; + emacs_value lisp_offset = args[2]; + emacs_value lisp_page_size = args[3]; + + if (NILP (env, + emp_funcall (env, "file-name-absolute-p", 1, lisp_dbpath))) + { + emp_signal_message1 (env, "xapian-lite-file-error", + "DBPATH is not a absolute path"); + return NULL; + } + + lisp_dbpath = emp_funcall (env, "expand-file-name", 1, lisp_dbpath); + + string term = copy_string (env, lisp_term); + string dbpath = copy_string (env, lisp_dbpath); + int offset = env->extract_integer (env, lisp_offset); + int page_size = env->extract_integer (env, lisp_page_size); + CHECK_EXIT (env); + + vector result; + try + { + result = query_term (term, dbpath, offset, page_size); + } + catch (Xapian::Error &e) + { + emp_signal_message1 (env, "xapian-lite-lib-error", + e.get_description().c_str()); + return NULL; + } + catch (exception &e) + { + emp_signal_message1 (env, "xapian-lite-error", + "Something went wrong"); + return NULL; + } + + vector::iterator it; + emacs_value ret = emp_intern (env, "nil"); + for (it = result.begin(); it != result.end(); it++) { + ret = emp_funcall (env, "cons", 2, + env->make_string + (env, it->c_str(), strlen(it->c_str())), + ret); + CHECK_EXIT (env); + } + return emp_funcall (env, "reverse", 1, ret); +} + +int __declspec(dllexport) +emacs_module_init (struct emacs_runtime *ert) EMACS_NOEXCEPT +{ + emacs_env *env = ert->get_environment (ert); + + emp_define_error (env, "xapian-lite-error", + "Generic xapian-lite error", "error"); + emp_define_error (env, "xapian-lite-lib-error", + "Xapian library error", "xapian-lite-error"); + emp_define_error (env, "xapian-lite-database-corrupt-error", + "Xapian library error", "xapian-lite-lib-error"); + emp_define_error (env, "xapian-lite-database-lock-error", + "Xapian library error", "xapian-lite-lib-error"); + emp_define_error (env, "xapian-lite-file-error", + "Cannot open file", "xapian-lite-error"); + + emp_define_function(env, "xapian-lite-reindex-file", 2, 3, + &Fxapian_lite_reindex_file, + xapian_lite_reindex_file_doc); + emp_define_function(env, "xapian-lite-query-term", 4, 4, + &Fxapian_lite_query_term, + xapian_lite_query_term_doc); + + emp_provide (env, "xapian-lite"); + + /* Return 0 to indicate module loaded successfully. */ + return 0; +} diff --git a/org/xeft/xapian-lite.dll b/org/xeft/xapian-lite.dll new file mode 100644 index 0000000000000000000000000000000000000000..a3a5a4e6783a6c82cb68f5fed4bc85c00325941d GIT binary patch literal 249430 zcmeFa3w)Ht)jvL)WFZ7_gA$4N6+wgCb~krK-Gr<@kpOE#6tA1x2BNvFn_URr8cb4I z*KJcPEn3vn`nFiGMr{#qH31Yvs}XI*OWSyBH%3dmVZ7A*zh`Ei-Dfu&l$iGOzW<;3 za5(eKoH^&rnKNhRnP;A7mt5S=;u&KJfT1DAwgc0jmH&U}|C&*J#7U1H!G1sCPbY8J z75(Yt<*RBMjV^b?N_Tmkv9i3rzQJRxa2nm-dSgw!v0zz=v96)YIdAghq#2U6-Nsne z52vz{S>r3&KXj~nB4Y#WRO7_BxYJlQh`s>CF=jjpIGHMKO2VZ`BSCu8pPq>5uM>D2 zV}J4{pj~A;)a2QsE+sjW5DMk=GWO|0Tp4xjs}m_#{&g~TX_9&Y43?WUm(j_x+ZaoW z&ct=|3>`~Um5ot8&*N(g($n2tmW&kz27NGgFqIjNL z5UwJ-wF9OC>H(TRE4q1}OVIH)qOEKa;06s5l2)Q?5_Abv-UT=U@FRfc&q{O^jg6G$ zg5_BcrH21ClYUOYkK4Q#G-m-e0W^PB;#VW+c%Ptecz3?0Awm*#UGo+dFQ?oU^q&=U zWEb)s&7YO%JhfauGBLADy00`uNLq=`?W}F6WTLOp=bYcy!|3woXNbIBl1Gi^eGIJm zvoe-H-<->HPNq(7M)_*sgcx+`W={9OG(upS3UqZ0x{ORtw};A&pgR`$mKb!I*_c6cq=(0_m?iebvgAV`L_84?ILchWkl$U|%Xy8X<(B%rb)5-suK}Y`A6NAod zPDhCl)gk4+Gn|fSzW*D03n+ofpFhc1-|yj0hLfx;uQYFY>4g`RC@t@&DZUq$_?~Lp z=e^pme;-6`UDsdK64WXF6RQ1;nLl`vy2+=15k*D*nO98JvHrwIf|yP6hF(^D!2!ka zz!{3~X{D{p8&v#f=An{*e9K^*q4jB0eB>nj7rZA+x{t3%Y1x%_$>omA@-EH0Y^UL* zR^Hf_FLd6&I~9MzO!7~N?!SqUE7AM`WF9;UGE+!zCHHAV>yL?Tqyp(A{Vnqcia)a( zHT~mS26aNde_UIaq4io+k(ob1vh4%UWr|;a4i~~u*E4yEFZd`82F3UJP*02gtv@oR z;|||fI5ac_`#pl3U={4QdpyXPa$|bm=_si-oK(Qr6J(?Qb+{1iF?T69#`kXuT`T^S z8ZK;dG0^UDL>{~g%=)T{M?1=AiSi?Xf^8&b9I4ZHJ}(zmyqqdzBBMRjdq$Y&ejlm* ze4cOW4BtydzTN1`bN%|0p)VTd2)a&Vtk5^;drk3u82lB+H9GrsL+boFFddZxi-y$tWY!yhL&`t;@1Y^se4P1%{vFlrYI%_28X5ej zoV(8Z!{Es?xOi?$(%@vFL6_H$O6Gl(bp*0P7azg{)C=vE2wy* zd`|`EaBU2286Y%&&^K454?YAs-&1`Pd4?4Y)_Lv*!6aN)SI<+s3-;Gvv>rBx)-ds% zUq_U}4vKFtTwvSdDEx^9Gc7JA5am0P@6PtfzfwN5OiG}3hDq@)F)F^&v?8C)RODNd zqxfy;wf2puuLPD1DE`tk$}03dleeA3E57c$p?xg_y4EgF63^-T2W9(h11JjKi$JRQ zcHk16lR5H}3wuk5i{f7)1z1<`>p)5soS*aTp;5|(hsugHWyBI%kdUF5xZM7$kcDg8 zpr|HvCc*y1q5w*y>Pvjxg}yyny-!93P?6kV5C-Fy!n%U@MoFi0-) zQ`<$FD@^goDCH^CUshFKsJ{X>sAuctZRqZeC&4qgVB5fM+gDJB_0RU(zEUzbf^4oqQ%Bq-y`;W&_Tgsl2=&UvRO-QFQu4;yv|jies$99~y;&MP6`y>te=4Z85C z8%X){TA_k1dcc(u3nM8NM4*tvAnZ$MV|VZ> zL<*rUw|U5yd}x~(p+vQr*XKh;@p)#z3)N=O?UfQpIZ8HT3kXR);mxr7e3UPF;YoBfFrgNp!&h(2?#|J}%b1zY%Ry0$XP^Qxkmw6J7c?hp1*AdE_`iYQI@}r zSi_dTeweceo^=spt(a8qfc*ZPM-qX8w;xAkJC)rarPQmzwE0v?zYeI-*M-!y7An|5 zWy^`$&sBy(R!GTvgzAMNZt!lbtYs0^3Fs$C;Xffa7Inx0t{^(8A*a(xy0s)C&^Can z9g@fjg}$Cg&*#c@2e+)?r9mnUY#<_(qKUuSh77R1<``by8(rQa%fG||3Kd0p8)t)B zf52R@zU@mir}+u`pBY+jB!Pht#bWR!HQV-sa~>CIk}Q*jNLt;4GcV_{nbdhBm2;u8 zyzNO~`6z%Z`rtC)jf$@=k@ry>uZ|iw2>THmV(>N?7`AcM_)O+KRch#g>xsH&@SeYR za`O}9IZ*u)(nOd}3D|n`wo^wFv*!ixA5%FlUlEt>^0GG{*#Sh@KwF2*x{k8^;?loi zGtaekB9d)KF0bd`uu$_I zVQ$m3A4_{>-7cCQX#IgT@4ELzk8YjO%o`ji?*@?38s4=Ui%6NhV72kk7v-M6ZCt2Rcz5b^b206iRR5I<&R(R(A)l<<;BP@be>yW!nLwf({pF(B%ex z>2ITM)sF6)7ex@?!HKcr3Si}h%Kt_<>H8;f3fNMSX{5~UTmbwsP?{=Nq{yvMbCKr# zL`w)7#yf(aO%JahtWa%$Gt^d|$t!Q*1wu&@=pGQ4MnT7$|2z-58$^~^hdzrgeu&Jg@;yX6Godf-?R5ir?tBrD`**7%?Jn_&3M`;-bA1|6v_9 zp*!e7m(c9yZxeF5g9`Qo&`GDsEZDMvXagy&vdVb_LqmLR)0ZAkT5RBvNHEZfI7QM= z87AUg#GFVGXA2Q0%PRjw6(NGJO8laEJ{w6Jp+U#Vs=xXdHAvcHB-Q>UJAAbu<_&0h zf=nFugH+WPft1^8gpp1GS^vb4y{*Y~J3g*ks%4!6A4Jx{IBsK9hb9+)K(u0m z=$1Xy6ThR_Lf-5T2!m=1e?o;=`Vxb1SKWboe%$S>ZyVx{FS^igH-<@u_F;2$1Kj{8 z348LTfqw(nKFnx281Zrt&V>>NQmA)wXgyqNKxT&+EIY3hMmYxIhEHo9&{*)(YW(*v z(Z{cQCaK(R0ykB!L#y}*8e`aY2X9lLv;z!a@9y9)Cz0{!9s~PAxc-T_?!`5aU+efa zHZTIECd$k6y(;6$W_Z*3wy()XlGes)1)p4z<}&6Y-TVYmGS6{}v`--Ar7J{}k9(hd zYiIv4;i5GwMG+H4G<$ZCc5hGQ(z+=9ib(fT`dLb2oj;SW?!|N{^nU;);ki)gc1L6$ z*U`s03}ABXhXa10Uvsh&n6!aZ#u(iWP5o1+D3$xLW4wjb*6kg7EBGmHsY&{uIBx49 zV$nJb@6X6%11bL_9rsGKg!@A9=b-MJ&9?_}-zlo@=T$dw6tKC7sWO4;i}cG}IW?RJ zhv}4zk0cXGx?SKVgjh(((|ixEJ2+Pe=@mja3WV0Uy)=J^FKc3ms*|FA(@&y8H7^KB z+XY?wR!C`2lH$I$AV~|fbqb;U3axSrX*?H~UvkYN8+MYEmMf&Gq(QJ1ZM%S^y~lU( za33$E4RA4hsKWLits>be8=Ry`cksY_s_l=Xs`pdXb-Ow+>iK*-`sqQ2XgiM|e0q=( zK<2=+M08T;6f4E6L17Y2u+YZ$Mbh=neKaam(@-TY&j(3_h`GLC%m~@=8Fe|2t;2&+F%?RO_gkBWMN;ha`r1Cx^o$@9?N?_;PNeBR z<{^6k2_Xr1InyofLv8AdVluBO>4#6UF^^0uU$%r}nPj-;SD;!;IdBUBoEm+ZBZ=8P_Cc{Uk~ z-+Oikf2AjyAZ2gl$^Cs(Xz;3cP`uAkcnu0Qvj*?Im63T?Vp0;B$9wZL&Uiq*&6;>G>)^PpmwGM~9{CmVqj4*9arp*k74DrLxCN^ce2yi{he@9STM-_!k?I?g z!Pp|C@$f792jTIhN|KVYx=e8Dfx$%8uOPl}7gg1SN9|J#{>Z_OyB6yzPToNN%XK9E_n@O)VeZt*ZieT)>)FS}F7>W6Ig$z8Z~ zi7cgK3Bya(x-V$z(&JWa4dHP$o>t>wb<1@|ouTbMXt?gWG&(V8XzSqW>i>+VFx1Mm zsI3I_tK@p*wO+Ii9wJK@)8Gh~KF&+U?O7^}hGB&>1zn(6csa472QJ%XRl68=uvK^P z9U$IY*wCm+7H!SZ+~++?nm+pMo5?SmJz% zEfPo72p6b0N@ji)nNc^D0cT0SW#5YLnmbgOVVu|x5wn#i(;v*%FllY^L zrF@C^NxVbi2PM8+;#(xXL1MSW6%rRqoG-CO;^`6}DRG>{AAKa`zb5fsi652tw-Vnj z@l6t6Epf5L`4U?so-Xl`630pW(TB3V67Q3Ehr|y`e7D56NPL6DZiy=-E|xf7VvEGn zB|cK(IEg>P^v(NQ;(ZeDkoZA~@0R!$iEohDEpdg!#S-UBY>{}n#79aTC-FxIq*63b+-%-d2-vH@fdx3TKaTK;P@TNtUXG=vty9XWepvrwDcz` zxgUE!#f{GOG+g;HufCmc(8(QU?h}4uu7~1}C*#GzZY?{}zQ1`t zh^VL@APT-dP9vXKh7T8!eau~vCxp0uIg0ODB~UP9#UrE|O${rGd<8Sgim+N-r683S zdwdE1Xev3L*>n7pr?HjcW3w;QNqZ~}o>4n9V=^K=>tCvgUXPoOYZhX2|PCpDuo z6NftNN@eD@jYRlt+d;#RrooPIht0^bnV(Twx>C^@!k$HbR+PKbeH7NziT>imLR}9o z4L95bR(UPmIHu5*mY4gXcRxK_7tbqq!%l|Qe}DiiOG!xJ_-#t}+wqDHR87X+qy`Hf zh5IP~g_H8On}{+n2`d8qX`CAKDHhDBsY59zy^kW|b{%N^1v3f)_O6z|eFK z^`-a77X5*r!#mK`b{zhob80wlsQ9s~toWDD$jRGoMMXZn78nY;l=u^#0=eQ_oLK04 zRq>T152gIRpS;bxZ7Agiq~Nk2m(gQ@U=@}yyuS|iolH*b*WZMy{tFX9vJoUj{({uh z;8}c)^cwQ2kQbbf%l3#4XQg=;=3PLCEQR3}ez2&(KQjZ|Y;Es&rYQkuB6O!ika?Hq z;Q)&m2fOu0P|Kixa27n8jua&{1D--(!sUAhg15kcMgF;0!VPdtk`9UQ*8lBiC?$ef zyU3qRickL%vWl=?owZ+S861CoyB`B|aH8SI*MPS_9>#5}fz$aHB~bEmN|q#ITo}aD z@s^#FigNo6Ef>HBMgGirkW%<0(mwzU^)Ih(H*CvZ_huJFT3Zhqd~{M#S@%5NEErN| zJzb=GzU>+BG-ch_n0%Sx$IBp6Y5BU@aI=k4zR!d8xR183&&H4vVtxcVf4qNjqW_wN zBHe&t+lxgwV5a-5m%cjQOa5dCd{okMRqYdzoeTE;EP+$8q zTl#Pm_H`w2-B&U$=&u3+e7nGp4#oLVj1>5?0vG#y=r4IKyWpKW(+bX*c@D^~?dh8Y zHt8nN}H2zw?uo~JvCq@0)qCLXg-fE@oU{g6AJu>1f zBs_30)=$Cvu(`+e@=Kev9b8*f$d~5SV?`7G7BY&VDEI5@pWIH_I86J4KTv=C4b^?w zeYIT2W=Q3|vkx9lRKg0l~ z6)Kz}UGcdPIWn)ON{IF!4y8=w`sj;DMc;>t?}U7wT7;OCh@$?jEqjLg@0IhHUq6Rx zK1<4SSvWW?=0`A{C0GolY)8;l{F6Vwga2gA38a{eKD1k$!zXFY;FV^*Ko4oU!*| zbma`BsVh_EJcjv(`YCuTZQ7u-4!})}M6+A}=}#aVf4lV`%hXWHhIg@msD-Ns&q#<+ zEch6FvHzzb{lMl`)EY{;hH98lBl0{i)-UUFmt5>MD3#|gRCF({%cVD(Jc;OyW)e!} zYf~r28E)-DmX#@$c_8aHJe)VR0RN3cSk|RhQ=qWq1r#{TkPi(KFt1#@Gp&Cz*B8M`$^FuE zntJH!BbwQ(Aqu|t7TsYm_CVSWvo(aA^a(UTjw&Y-Z;zOYEQW8H$tey(13HQ98) z@NhnLY(8~toaop&s_IH$c@H|4`^7=)X-32t49h1JS~jCgmE0b05M8PnVOW3q_0L=c z`TIipE95`Y%_byPF#-ShVQwsyjlc^EUcwWg1 zZ36p{Hu2k=$D=*#mrw_`kEp!|)i1|{v|RQ>jOp0UAqm%(?J29`(a zciyBWzC9Q&?g<6)`H4ljU0A$A!J^!K-nX#2C2^R4i*)<@lW4uj`(i5GDglGiJKk@f zgn{u&+cQ6Sx4^GVTB3WEDuiRe>u+l=`2NA^Z_<64I)2Z$D`(RCd`h5bMymT5WnCY? zYMH>4xje;7*ZfnN)vX|N#YK_q+WAd|CNKIrG~bmW1kjm{wr32jYiJd;ZI-q51Fr*4 zN9$+Iqu5>X?QX$(p!G&t6Jh;q{MgV?MCaP$agk=2vN+YZDADk6!HhZ8zD1LeXYwt= z+NEGdj&IRa!gj^CC=IKNmR)6ZxH|9Bonn6xdlS!_+;=oC_&Ue1?Ht|0OFan-=X=`> z+b)Px0_Vgve{rej2d(?OQ~Qs@`qWN?P1m~5a|GqDZ2n@t_dUunY)fw4XJ~y66A%i# z)B2C4_!muoc8IV3Kg-Z8Z2?*w59&M)xa%44_V6xtw z2S-ECPton<%Q*7u@Vi{kD!x~^cRTyOl=zD>3qJR$(*5svrTg6kJOT+WB$gPYHaHcuH7U9vxFq*) zk#X-)%+Kr*BmN7%iT{WSdwi%cl?w|r(@KGA@Y{Q#=4KFL_0x9^j2)WqUJzw?O$Dx; zO%_o6jznemx!eZ4&@g3uu$IcPoSPxHA?VJP-?<;4J9oY_(f9!k4@^>`jkLZC54fS3 zkN#Ecv;6PEs=p1*Op^M2@rKazJ&9kFc(25dO8i@iZ)9^r9R*9ewq|}9Z3fA4h)5F9V2odynRi^-NJa9^S zRR^r!#{){b#tRQVdtz8Gbqqz<*VgS6byKmGLD}D2pEn-oV^L3)6wP1g7k;ziTKFuO zr(#Eq7BEP0F~kWAC;4uU=4}oI`|HdzFRUiwFKGRQjEW8qZm0u{9*Xh6uGIzu3 z@PNz0zDqWU{kl{dneJ738jkxBdS3*W`}idZzzrmJX1arA(4E?uhK=(mds@{uP^i7) z4HUFlJ`!7xdh`~GAFsF3Q&MP$qh<2+=&$e)nwa7h60RRTo1pfO(1Zy45{=ljoW$v& z5rt(q00HIWno{i zQfQ+(yyt~QBjjo5!>361{xj@tq45B@;aAuA!7~0T9*#r_OY-;sa7FL(@X?yA+DEmw zcW(7w(i zM)3Erz8e1%kxTO9`Uw{WJIp=`hA>p~B`NgMX#jprubD!~7SXW-QY}9I#jYw`#HeUr zuMODVp&Gd}XrpwA(T{@|fzlw_WD>C{Z|FrI-q!TvR4iV?^iquBLBqwL_UgO;jD3I} zKeiyIdy?_4rtdW{d*c-D$kC1+s=E+%iI(hkT@Ldc$=T1QQ)3irb1*#pmr-N5YJUSa z-~Rq%#e+ zL~sY!L-6Z{t0-Z95AsX6{Z5=)3^5w+44PC+)U}hLoJ64&e{Kgi7J%12!F5&Q65PkSm1sQSAr*2&`g^@nMa1erHY2+gGCeR#6^3&a_J(5JGlDbeHy4{Y$owP9}hV9$2c)40VyT zhv;ufpF{Lu9srJ9ZV@qk1HQUhNMJT|{{lKc=CJXkU~+1o4Pgl^elN%G>`9<=dmn!(%sDED!ul zT^w-!(pD61Lf3M#!^F);xtzgoiC%#aJs--lAEN4`8t;bn$znQH=X>5i!G>e_AR{kG zrVgTb+K&fR;tEe8Lbo-lxzze%JU~YtU+EsjE5ATN<34z_VC=?`L$RW`P)6=Se#^pL z1IaxkGzp^#K0ltfqMry!V@XKSUL|}J%QsGliHr5m<7*UNe1^^FzKbs*`$3OMUd|Ho z6#2b4N=eTQau7a>p5E1*|- zN0f*A6K$doZ=g-WoYIv4ijUSRy1uqOe7((AX>o!VcSZGXm7<6`YliM)H9HBS&76OD z{Gfj&6GFI>5lP$u)3qu?I=s}+8k#@lJt?-}NIcye^pfpwr8!Re4PCd=wYXX3o$A3g z*~}U%-l?%p+%568?xvU4NCa#s@5}iJ47G9gyVH+`c?Iq2zuH7 z+ck7C=5q{b6ZAHMEuVx6xqF4bY%u}jP@Juk)5%665L*P3-MxCTKH}Z?Kd**eK7v7nel(uRtI+>%j8Gw( zK4N`&5OzSoz$9+i)`jPt{QaF{6?LgKo(dcF!8Gs=F2;Xc7*`bUDeRA_;mG}iTag`) z7~wy`jj3=VzNp~+b?;QVZv4~`eQ zKY@O>!G7BD86;Nc(fKprkLd2dr7jiw8@53spEtd)b0V!lBKU15`{57I1i(x7ZW8ss zk1u?&_!TnFnF8L>T+3aBcllPF~%PhL2Tp`4=~;-%-!Kx zh)ljWFo!hZ;VeWW{mL!k8CAhi4|;=v7UTBB{<&)10eUVNzBxDIN)7kR+X# zzrs1}c3s@BNA7pPO|b7Oj#S^rU#G<@v~t?Jk7UTQDKu<&+Ysx~Vco*T=pn zqM6RmsS`r?F&FSlI#d|ElFTP-oRlDz3rhHgg4=V|uvd>sE)M4ZJV!mhNb)}79Zek% zW5E>YbDpHbsMwRCG{~-|+b^GO4`yqCQ12jrJ(S)lqN=p%*&WQM=I?tGWp8-W*e|x$VfZ~nvyDzT?Y8yE>6*)}AalS3QshJQvMfztOGv^14iL(^SM%8s z@>*;?veUbRGb1an=9L*LL#@(axbm55Wzi7SFHTlw_|hA2&y?V3pERrw+^D$+%aZ}gAHZ+1{XrE=GJ4>pyx%g7u#a zYwz;lRcfO5-o(;kCyy54RpBWJ%Mq)>I6mIsi`~J`{w5R)FGHmDqU<5{OC;8+FNC%n zq(96iLrKEBG!0SvgC3Aa`tQ@WA)VTe>n5sZHs*;H>$7xhkOoT(|CC~yd%|{=_Oz<= zoml_zudDHXe4Gp5k!k>kuMDfR5tjx?;qVG(^9$6z(B9nxbV(mmkmBCNb$^Pu;?o{? zsE3f-&IdvOu@6R#Q+LymJ^~tYiwQ+UF9lu1c(*MmqE=Wy8RbhZZ(w+ln`@P~Ep zuSbpWXWTGJ;v5N{lGB+0pZGdf5ygHrrvtlOo7G!=3X}W{GTBso46&Eg{rGv(A2Gat zK4f=NRtKK!s^2f9(Wl@Wk4pxNbY1J|wv3ZpMqxp`OU|$7!o1i6m;&qc(4JOf)CQpU zS1=LHc04A|N8|dQqQ*#auNNuAgDyy-8DI&dmIBiwPF`a&v_w&8a`^+lMpuCn&jBRW zXMX|}Z(n!t45C2HRClW%_-+D@U+wcEBwcCnZ%A@w@aLRz5^BSw7}iZ?Qc#~MB|FMFMOW!*07aXt@> z^m#wyn14<)Y$lY;nP!T=y zd>=}?-h~+@B8Jq{6eGszL$8TJ$Ul=93LmkaPZZ$z8NQynp6+79N;h(4N^`;y>?-6K z{zxCrRQ2H2DMogL5`8rJ3nk`BCBlJB*sDylpwSGK0St`6h@pa^P#Prdw5siYmfJ?7 z+FvL~PCOT5I|)`CfSL#d@LFFftuT7I)A{HHR9g2e=~6v8Y)(_OIZq0=q)Ofy;@nom zjz+|MhWR;kz5@N(Nm^0dT9OLq$7?4THP7%jq4iJAq%KF1{xP%Y{1&{C7;CjNO$n;^|qnr>xLMew8kp#v;BNOIt@)m(`^Hf!s8*LJNR2*pY45^r=KoA z-iuz`R*Hve@9TOnX#`69K@QFE!O*$9%3dOSnI^798*5S336L9J}R5ge-D&P4)wWqeIRd-Ase6D#RHDM%~X|k01+H9;V6&p z{50*-AMf}Sz4MmR{aze@1kf8tV>&RkjSE7u8#%MO`G=4b@aSF!(_!`L@d0>>=W{`d z{{B*W*qTgB?|}ZN@FT;v1l{^LYBKZ@^Cgu{UPC@ck7adxus^;&uBGm)ID{W#rQyaU z6cCY|usRPEI3d%eIuM^r!eEw)ajzF%D`aBeUE*7idJ+e)-2pE3r#_N}*)O*MY1wwGw3 z78EDlk)-n}jpHcI{ch(#yT2i113-yzFUcD$hFbjg&K zyEF?aEp`F1W=8~Ko5|Hs+_v^v%P^|lN9 z+iw*(d6VoHiQ6Ual(~y>qV9?QU?h>PDxBga|Q&ZIo~Hc-{3i^(&3FHH|J~qsL8Y zPlIvM?Ah{)HI=(+%IoLW)_7E2RGYD4VqfKKGFI2rI*sKXqdjlAVq8<>S!JXj7ON<4 z~ zQ`1myY^bkYo8)#1N##OTO|`Mc!`W0go%Qs?cQw^D&MISLO?{=)SXtfN*THszHbFKMaL zK&WSxlWR2xd4jyAmPqjfee;r%it?78OJa$tw!D6&w|u43=xs#3inTmbDC(X=E%G>P zYa5L#xV??58rHx{XnK_wjat*-uHsgs1bW6<*Eq-MoVRk`9Aml5RqJdtE}VOoNX|>j zgZa=Lbg9SKP;GQIG&a^$ppR4q8jbh?L9%~!gSWm4U6-`V<8d`Em_J|i^}Gi6%K23d zm5uYE3h?|@p1Rukl`w3hP|jg?m`k9;JSv!%l)u&pUn}?4dgd6_22Xd^Pe=RHs_TVc z2sbdE+TfyYEw2@Xyjx~5@}Rl(<#o=vsz=OqF>)KxV;*C9$W^!xMffDIESy{l;oUsE zlC#NGUSBmgB#b%hnTIVBzoppVf+wkc$^BdF_o_qBt!!|+y)KU&R3p>j7w)c*SH(%S2A|-hM^$`;C8?epzdcwL$3o!(D`1cdo8-mZCq|E zUP8b6sA@%nPc(gAlD&K-F@*^ly)IXS+ap|U**WKw*p?g7esssVwz(yR7u)6;Wp1O( zSxF-ZKQIaROltI$yU~*owA?g}HCJP(o&yb>P33j)I(RL{1g}LB%_bw-;s&ERNwEaN zi@mk*d>Sxr_@;@=Fq>#F!uqIAGU*yPsj&frA1+i#B6y5Jlc|T?&PKSJv8ut@Sbv%a zx{#lj*RMqwB#kiMX$;pjtVYwT-3@g~R7tg!XqWU5#EV*Q705!I`3PQJUh8#!KX{Y4 zpHZklz%o`g)YTy#ivVD3TwCucZ$gN<(rL_FS}@1BtXNof_WZf?l1hX_RmKYF(k&Yo zBqbT~m*3D(>x2l5KXMwn zC_;zNUsdjLB96N2zz3@E0$UTpwz002s2VXL;OC$UxL=B+su!eJ)SNI?h*yAvpv><1H%q&0=onYNxx=$wcrv z*IDm$Lj_H|5b7{#a|91}IYNLS*TyXx9T726PGLOOMHE~`L1Zo&NSn>$7$)qRN{3hi zEI@bHBk2{BFpVN(JNO5sGi;3-~lf#7B%_GsS-cpYcw^@ zY{I(GSXzH&eZ!i1qpQ*Bt!h9_gQ?2-stD^mPeWxxt&tpvmPs?K<_WD2U9SQ@Oue9` zCvA~ETnMyB+Z(We!Xmsyynl< z9NC|Pr$+WubJYFKs$WIY58NGjZM`k>I`sR+Qf8<02UUftDEaMCUWG#uJ%`# zELX34>|T_92oWRG!5L z9OeVW3=K)BJX@__ghiN!AS6j7RCE?7xdU|?BdS-f7dUkqFdTxd(!<=EsNYBW~$jr#f$j->g z$jvlmnlmk#>6sasnVDIc*_k<+xml(xbCxA5Ju4$CGb<}AJ1ZwEH`|nL&bDNyXJ=$* zW@lw*XXj++=9qHKIhLICoQ#~zoUEMeoSdB8T&S1}@ws4~i)y*ZifUHq^4vv>FgF)F zSEtW0=6h?fV#6)Zl9G!`Ec83(S0M7uGMX*(%=7S!oBl%d=0nn3zBB#d>hnGQq5JFq zx_u7U-~O++_i*+9mi7^&SnMwaPt~!WO=4eb0O@v3x>J*OY0}9uO?7)U`6f-;F4Kdc zG5$jCo2i2Tw_$AVO`;9HF6{FGzc2;+en|HKPsMrpRHTi-7C;)(R^VlTIY`@qF9n#8 zE(0zHWFYMVegH2e{JvY@Pm$h*G~tu+WVr`v!tVgyK$`IBcpEE-G~qV^pCC>62h*U> z4uOA%^i-q?=cYn0qzPA@2z`(y{O&1?bs|l8JPxVYcM6<^bTiU~OJ_57Gtz`Ti0eV5 z2|sNHpIrjKgR~WC!f_V7Fpe}~BVYy6gbx6!kS07i9Y45<^gt%ski{5#7Ip!?9gv80 z2k?`CsYrJL9{{8xJpg=FHuxdk4BP^kgLFIa=KvGZEQhfQ%wP+VruFKx3-MeU=^kL~ znXo<5gzpEqknRLFpM|eWQa-TdZ0JU5;95XC(k|dGz^zCVzRb$lMx@Jtdja<#O?X-! zW1EpS0{;}S73mJ(zW_Rk2Dm04eT}pWIBO9;r-!r^coE=nq6hvppbP0v;BA0iNOu9> zilx;aq5*yqu%GgQ{|e|ux)-?f9P|Uygr}Vg-#|K9VeCx6t+2Tj*ai4xufTU89YneV z_|JfhZtww4DFiLjM&PpnMMztLn*l43Cj1g$D$xU{oyXYmNYi(auLYzL4e-wZU0vW0 z{48J((mlWv7Blu0(#gQ{0qs;5co|?5(stmh0h@^r@O^+pq&tE80dw%!Z~!>92wje} z5jYF5{we4Q{7XPP(w)Eu0h^F!OE8uIRU`|z81M#kC50e*NHV}qdY0@mAspFw+pmjd>H#tz&J zXr(p+KL%(*x(oO{zyNd@0R9?a2Y+@xV^aVF;9~@?1iXW^3wRx%7d+d6p8_N!zX$kl zfP}p$1O5cC8EIC`*ogou(njEm04qRW2K+Lh3h7>8eF^#zG|9kc07X(RC80Cvg;KH)<61N1in zF9)bx*hmWl#jOe0H1OZ>;|4D z;L8DXKvM?%Bft!z2fh=~3;*c=-VK;Ub%756wvsI1#TO$!fW{8|OF%31=>&cYFbMtw zz(?U*T$@nW2;2(jM7kaLHNa-j^a39Md;!%u;6i>6@Z`%t1D?si3jkF}TY*;rGC=PFeiX1D-RPJIDfUWG64 zfyM>A9Z-dI7cg@|9@2zQ0vwMv8iBtAOo9%q8sm2*=1fYjLTm?AfuA_W=q(GXOlj4tZ1;cnP4HXn?N;JWlom{uw|)TRMP$ z2dF}RC-5Hudywt|KBXS>JJKfLUjllF2KXgF2I}?#_XDg*4*-7&uv1$a&~Jb@Kwk!Y zB_IbnxPWg5w3A%my@2hoc@OZH06X%Ti?P{&Ymqhqp9Q!SZLtC`1*C$%9r$v9k@y4G z12VwJ1$;fA2kBXmh*hR8{zXSwH&#N#m09GL13cL$oMSc(P=KvS-nH%F8 zu$k-$d_p694rwFsJix#%jiXUGbZ;C79c8=ClwYkQA5$&5iE^~5xVnu=}zAVm#wny`e)+_?Nr6WLwk_3J0H7su;+li0!Wxk&Fx)PIu1 zzDT6}O-XHWG?Wdh5o^6omf<}$KQFug|?x^`nqh~}vtNbSEXTqKq)t+jft2mY%HI7b+jHAg{=3uR*yHEFO zJo{HX`IRa|#Tq-ruz8|IpUal2{%AQwJECo+(nhyc%~$0|W6cknLw6upxyJP4YZ4>o6+QzZkqtZspj76*Er}2Xk*DxdTj3x6> zd9lifcPt$bN2Zoml@UFbV$o>xv}u)x);8MmkoR^u6%|I{UD7G}RH&!WAJ?AIJ0-ohh`w z*3ywa-vqQ?e^9et*YZo$v&4LLwI}sfbpYS*|Nh^$K*_A}*bTz|-9Lq&J&80yS}$WW z-e%1BkMOe_dRFtmRPk(vp2N`d8hYMC&uM5+-U^_-_zplXz*vYpKr^5hKzr%UfDQod znI|ts9>5NuJ@HP!0D$(sY0sPXx@nJ__O@wHoA$D451aO`Y0sMWs%ej!_NHl1n)agA zJ!slzWZFZfy<^%lroCd?Bc{D!+7qU|VA=zwy2Kzp{G0NSIay;<6mrM+0%gQdM!+Hy;0f|rM*zv1EsxB+ViBnPTJ$7y-nKFq`geq!=$}S+Owp+O4_5Oy-C`Wq`gSm zgQUGj+H<76M%rU6D+3Sg7t&rL?IF_MA?+EK0XhJ*H%NPev=^xE0n(ly?e);@bFB)`YlT)=sN20%ApdO!B10TqBB0)7GL1ndFKe4nwE zfL{aN0-X2(p34Dl0Q?s40^l>iN&mvy8gL`vD?r@==m^LgU~C8ARlrApq7NYxkp2;4 z{{xuzF`hpG-Ue7dVeB@*UjWIUVu%550Q3SZ2eFq2cn&b>GuQ&q4(I_S4`Oc|Pyx6B za3`P}@D?EPbGQd!FTnf-_Pqh00WSLz``~~xzGCb_z_I^EUBG*QMPK8Y1mGjUxkG3> zK+kln4DcM_WSx$=0XqS+4j2H`>vil|fO(vb-3~|@ zuVb45lkj`<4S**B$@n$T3czE4$w@kP3E+2tuK-IX>evRrzX8`xf^0zIWF5N_&1Hcif&;xJ|;6lLFfS&;# z0=xwH1dw`+j?Dp_2`B@+3`jdx$F2o@3b^t(v>kBM@wg)g>;=35pcx^K#j^zT$~ZP2 z?*mO>No*pU#3r*Nm;vtwO<_l}qu5lI!j5LsSSmY)9m|em$FmdIiR>h1WGA!f>=ZVG z&19#t(^wjt#ZG6l*&H^P&13VKiJ6&&rLzo{$+B2B9(Cuk1?&vAke$iSVrMfe%VYU$ z5i4Leb`Cq2DXfs4#}>09ObSccGG<3`FJ>idIV)usunXA=b`iUnU4n3V8M~Z0;78>M zdzJW!b0|a-biBg2udyl7&mLg(D$;{Yz=E-YuVN88g?yqQm$t|V9jhD zyMeW^R@TOT$ZllonUCGX{Oo2HU_W9%X6@`J>=yP@b}QS!e#U;zHnQ8;?d%t96T5@m z$?jqu?3e6r_A7P|`ycje_8YdD-OGN@2KK46yKkH-)?f4rl#phb70pxRMCWL_jYl z;mq=Iil(bWG&n9rZ($9mA&M;GQi1buRr0L$A;`oVTn;B4lpIc0VsYRf@ysQjl30Cn zYB+sUlf9tf9J@JFw2hzFb$H5GmY5t(_L2;TeYH)?ud!w&-qUf^;S4m6J)4K~6U3Gb z5?lm9X^gHhOt9TvXAIWvhv(S?a0IQB}yo3t8wS z8~l^Z7?!Ukl6rcw9gfPTCOTfKJ?LLpS>iF9D_51f9Ugaiji<4&(pFNA|7Y4o5a1>*a8O2Rx4(E_l&`{~EgKL|!aID`^Azod98QgWFG=s**IfXp* zl-HWm5ro{{#+ubmhs)zGgpUb@LKHSzM&p`tm#r9scC;K|hh^WbIv-}Te?qE#8|9;{ zeOPsQr$n^cd6gK4W{Y-o%1EXTi4ak=A4SpCJHG|95Yb4?avhG9^qr}nZti`EnM?*ai$7o53Ia&^) zKs|rc)`3I%JdPFGmzEs<_$~2diPv}vOR^l$0skXp@K{rl{ofU;hRCpyvK*L-8p;c6 zYbwvHfm+K=4*NOF9rne`%?`UA#xHO!w>ZeSC7BNU+LGKTh3jg@(kM0zV06zgyDxV@ zFPk|NFE3X+@ornC)8Sd`A_pw7IZBi$o$8%qs#67@yGB;aR&p*^V6hzFe3nah)>nyn zGa@^)!ijfIBMWlWIBd4y^&7Ki&cmfBAQQ;{wF02DgKZjXCb{or0oHCZdix9dD$Gph%08|A>fOysJo3 zmb+pJ%EaJyt_hcnsyQ~Z8A8<8B*U#38|FE~8Rue5;{D6I3U76IGKjGpv}HQ4!ds3M zm-yYr(#7b$Z(TV{ST5Y2Z(WmYUXG~IP(6yuq2UyssJ@erS)feiDre=D4$RE9wQ^pD zCDdu~yKv8-S29D591D(7-Ho9#8vW#ap2=r|@4$nv6h~d(LZ0^>b+8EQj#dYC#X9Qx zs{CR1TG&*$iZ&h8slP^!vEg@RW{al*E=V_OVwJ$}3y0HLYU!S=azsAqp#e3#u6jdR zTnP>|H^d-`T$v6h$i{~PTzFTXFRv^Peq#zMi=)7ac+;@fQSNOjEUGZsY?xcCMBeC@ zHa6Sp`)qYO zMhz%%EyFH_TndbhtSIjuGp1u}5smL{87|MzAL@p`h zjbkV$?2;rxotDQ^KC1qQdWTso`^`BjNw{ouHvns~wM#bup$Ih=x6asxDG6`GXuNLBn9J`tiKQ&oUHC`- zCW7pkNWs%Q=0Z=KZLF<9&tSwR5e)#bQ#8g7 zWT9JAbx4JWnR>4>7Ty!y%Y{yx=OF#&L%gLrgfVqY&~Q zU49U;5SzbZ>j|OTV=>{2e{0I!RiQm}I#MHI`FE6qKsv$s<-}bDFeXU$4sa=+f z%~otz;zWvgNLJ#(s-?<_&xO{S@d#dS#WqrmFT^tBiscT;AWVi-h|Fwq(2ft0uC`Z# zS~y7fgL3lD99?d5EK)zcIjT+j^GJl|#*MRdRN-MyIUX@{=cR4kO!fJ{dQY=F*0X3h z0sZq>vhe-Ks)jm8d1WQegGKuq_?dAoL44}6p?Xw$+n}^u27#RgkJLy*@py1l4|i#8 z$Rmefr>?pj<)d3{Vugn7e|~3-^(J3;(!vc#3ecAj0j~&)z*=`yI)9T8+${5(Q99#6 z>#oAlK9R7kE=CjhRwH!BPJGB6@P$$5N_x2V8fkaaqR9wl#>0$eI!SHv^P3Aa-WoD zSow%>bi;%{4U_@@JF=qTK+hJl#bCfOEz!{LG>dW;&Z}^?R(pSndDB@>2Ui_6)rBh> z-T2C^h%R{2OXC8Aox+RLR$;@jHu>mx&OGW$f6^?glsk(E#rFNI9aTt9yk_8 zQ8ZSV7RS}p88vo&h{4jBpDi2dJP&6>;l);xCse_*D26J6)yNIi@X4fAARp4p3lDub z98F(d7>Z9(-yx1Di~a_2L}82%5J!~9`T(((V5Bb&YiTm&SBEJt;>*LxDEa1AmK>s$ z;j$R53>U_3Ww?BVR)z=;(aI2wrj;ept-RnHTd6%5d-!KQZA-D%E~d|#W7FnJtnLWe zYT^Hf+P?bEGO@i_jtw)s(^20bZ|298r@YOjvm5Ho%(uM_I{f=w+tMt@xnA7;q9ybS z2?p4IS@763ccj2aM`^y{cKbhLhOtmcW1eq$XKEw5nw5AJ1f5yGX03fS6YmS*XO_G) zP2s+bxoW*Q@=L4WbL~Y~3FE|%hkjxRFSULDCoRxqq<8$p-{T8)ta%3BrPSnog1mO* z?bqbl&&0Qnk=Jx`xNgo_s5=wyK%NrL3nI^YTKHO(LwJ2!Wd0_3{ddtrq!7`8@+`Zoy|OP=Q=$__+d?cSBh_4#21E+ctjKbCBDdsJDlij_5*!Mt<&jZ zck7qeHiF#+w5`Ej)XQJdvWs8C!ncIg9R9v8zPZh?A<|I6-@M0HfSIGbu5qOUzxs#o zh&;>0jTF9qt>Y_GC|}1O(>dt(7g8N@alr{JLsooeN*%<|)@&d`mn5$Ao$DQwexPgeO4w z#t5bAiSQ*#c^YaNdp}`ywF^&>JowJbu?h4B+bHhHhP~T_?*fgIR|Y>|yqcLtTVKAV zVi{%qO#b6?I0%5hvqs5-4t>aGu({)eO>@Qj>@^K|cOAD(Q7^OGGTl{p!wF*ZRr=7c zIAyHC&joUYYN?EGIf*Cs_^c1UKdH%s)~?Vszo}wZg|F$HD%&-Y=QfCz!0S~p zmEuwkb%##71pF<0Fidw8HZ(ex;g_^<3+80cABG&EDSlV<2%%f1&`oFtv&2*`vk|Yl zRdTyH4yRsLV}-ZQ#VejRh08na4G0tUFMf%*fxmzWUmqnOIwezC?kcaW@vOz!7&d2= zd?F4I&>n|;ragbu{OGX1JV;<$iwJ}7jgOKSy%&KXg+o^Ah+r)HJMh8}MAPGJdhHw* z!?%b=%?mqe^g;u@tkejT(RJpw z|1S069(Rno{%ZEn=<4zi`|#-M;&c<*4ZR;7UHzIS^nUtO9&2+j0rJxl8MHJ*kI+P$ z8LLcA3U7(B@EH_-0ZkiyS1Aa;9Jxf|AiXO%c~0EP=i86HuIvf74~_{~`HeImRu z$j;&A7E`F4$CEI@xl!f#aZyJ@wS)h-D7{mAfheaFHe7a1Jsy75K)!n=|L_XYTq0=b zcuiP8x4V2Te+gSeFlzf&PM1q7!kT(Ko~OL$Qt|lY0^0be6JX^PHLK0^F#-P5=KSFr z7LnBrHumChI%-s7&2Un-;TYl>@$`b}j&)pIRcl{OV`@Co*hghY97{sWLoXI$30tZD zn&cE&#=oS1nF~Kv%%;ga1V5RB-`|jVyyeutxL(=RDF>8J^(`?53nT<}zRrFiHaKJ;YLx!!V+mC;;zLRF zi}5zbUKz2Dh3e2`MsI3~N}=ya*w|l3s%J5WWZ}FP9owQKENgLw#N;rilMdlL*ySH1 z@sVTZy^+f0xJje6t7_M8_%ISqplQFwWV>Q?)@HK~Av4_iXiMW8iHE2stS!H2q*`0O zgq_SI*F7wYXN=4zmpZPxwtQuyt)_;2HhW}R-aBWET+VxErM5i!^E_EqPE2-mE5KT{ zbi>Z6;F~P`?$}YKn&S%S)^_MRIMpJ~+=nWSuCMu634Lh_r9GXzXei4ICie!5vfy+PU z!5-7na3@p0n}K~GZCNJYq%vo68P3K^cMbo|=KM^4=$q?(KufPWMh;b|LrAh+am5vO z{%ybF5?t{l8bGshfwPj%l4Dr1PqjQk9Uav)^vh6ex{T0wk;LbdMmLI~$HK`PJBI>C zuC#CseX)t1IWi4*4%Ie%5a4-aZB50>O6=FrVq&g&UR5pjv!){dn_#i6c&V)@-6Cq> z`@N_$R#4Zty3*|toEL)c|6c*NZ}e1E&YnFti#OJew*GGh=qrBseQvr*?RU84_kSZ< zAUXE$!VhGL{4R+{s{Vcc_ZIlx0{;grfTu>2pyv8}+V8=~uJN62eB-?%wZqtv)?w<% z>9BSv9rljqj`og@j?EpN9r)li{Tu+Z-dlF>s(W4cHr<=N#kj?^#k$45rEH69OY@fY zEgf4rw{&gk+0wgZ03W(e-kQ4AxYe{ZXRCFqed~&?Wm{cao3=J@ZQr_aYsc2kt=qSD zZSC2*e{1j7;MU-M1NRNmhp{05-)IL){HZobza#SO(?GgzogSu`e-sye$DKW%7wgm zAfbPIw$T3!eKaR=E$Hj=Rr;QVg1$l+ZCE8Ce?^f>A59?2Um3An$h%{STHkuMkazHq z{c(9kh<=fvAAua--KYNO57Bo>di-oy%s)Z@B7PM_*sJqf(LaEv$%4L1(!VT@GYpd< z_;0CI={syf|C5GMiVQ(Ngs0h(zofr#1pXti`951(;AV_u{N4sPrs>?JrN%|Mxkj^P zo_U_hIJYh<1OIuv_0BU_I_sV8no3-jyDL|n*_4wtmkL)J=Pt7t=i*l2|6}h>;Ik^O z{_&Z+JzJJ1`wArNJ7EzJ0|r8Xuq7k`g4G5{0;C3rNf5MF5K+MutyXENP;INWwo+@g zYOA;wtF2qLb$P3Awbj-x*4o;g|MxpH_t_F?-~N61_y7Af`P}==IdkUBnKNf*&di-V zcP0*#KKW#kHpy3QKOKoHJw9#|K|Z8!=@rh9vAaO-1_Pa&H?1#Iw=J7^T6}w38TKR~ z#*N1)gca!7fSnO#$z+bJA{|Yw!+|7{;vX?1M`x52Neu%(ign&xV}?O9v5 zw56e@x~^faxM5Kw_xp&FHU5%#HJ@}Zj-*nq@u{2Vi z12(~U#BBXes;G;kuRuQ(l}op5T-)AVzovW5M(ls-?h*BxSz$a)Egk`MZ^VXXY=~JF z$+|8iM46ZvET)9s%eW0`sjgevRMXrfo^hgqVMidNte?1GGxpYG1=8oSzmxTo6r8Q$ zpsb&);2aG{W&IQd=W4i6)}N%{JPnu0`l$-e*YHS*mx6iRzE(g%AVS4d$R*pVd%8uT z2B*lz_Vv|0q9_plDPpG+oZBOcH8_J{O^+ziU?stMJ)%^Dvk1=b5d$<>MX<3)4AkHp zS)(%^q;r}p=XO_!GA-Kz024*I2I>J!5`#6+3}CVtqJh-_ri!5&*Z^Rf7^Zh3vY~C87^#8XvSIBuF-ik_WkXMw7_EUD5Yr{bXy71XI>cBF+zJLA zVw?sJ$%g4-ybga;Hk>Rb=xl{((q~6D;G}Fkkmwe5fyhOO5q74IVD)xm*HFDq zchJe4+rDRA9QOA8#$Hm%~PS~U) z52r->X_xD0*miJb?b11Qb&G|&J^e9=9GtVNsYbY$72#V!h-CY9FE74>F>^I^#Q=Ot z30;D-u5^MW4|;rhM_pR>yT04nch+;A+;#7WaHP&AJ+~LN4f7zd;;!qdNcv#ZXQBkQ?|5;5?Gg$Cbt62Q<9WhciX~kI=T) z`*4d0;$3iVhC` z^l8mYmb5g^UQyFByP;L{b?onu)D2{5BE7js`T~{kRc%i!&&mh;W!{RHx{ir%ObmIK%O{4mz z!RBsZY1*_H&q>18s0=w5&kXByYDcGzDqJkFt0{A1i6;wBqw-B38qnzqHQ2IbP0yJk zsBxtcr6j-Y_J~j*G7b7Gi!^WC7{`VK5mwv5Oux`mL?jSq%^k!Crc$+=7I43csA&gf z)SeN@pM%aIvY>qp)|SO7fgnK$aAx>6^lZOEt)hsxID;7l}e+rt|7koHFl;&l92T=A> zjIAO+tKzQ&y1LuX76lqeaaZQoHa0cPPV`-(xmmePHGo{j02yYJ_Q;*VF zqZ9R7oK?ymC3}N`OEfG>$7VH>Ze9mG6PmIZmlb4>lf42C3cZl=SwZCpm9#9yd#|-~ z#PktaL*7A}xYM#2inX=z&A*it=v@!~nNs~3q^1{R+Lf*HA_nt|IX zb-3w1m&H~gQHf)9s_SZ();7(V-B=?64&Bb+bs3j{)`~~8ELpy|skUhW`mnh*b3rn5 z1^Tn4%a@oGA!&Iz8A!4MFIO+AZ(5~;OP4QREF_V&lZ2&y@vV6NHcSzY`kgLZ{#LIN z9)IV~#wZwIXbwz51Qo8fv3_<_^#TzhRMXH<*C4`*gyUy~AxKj@dufwMBd~nwqNQ~! zmx^=(b8D)hoW&x8KA)nBtpig;jv|>Zay4SQ$WsWg&DR)&3K&x3ZXrWZ zNFy{OiU@FgEoPu`_Pm$m_TqK zzW^gj7>)|>d&qV;Q$|YdpCg?A3T`tHH-hEa*3q&?$l*gL0HMKig&d*4e5P!T5w?Rjd4z0y3^mLpDu{)RW$W?XkFGAYcTC91iovL^R^}&8 z8OS1mUDan*f>C|~|4rJS7Cu;g*4wCZ6+1NFT&P~p5`53j|D>OYl;=^x4j6!=ZuA3% zN4aw6c6Kg7y@|Bl*_@DGLz=TNh&ZYX4Iw0ELjaP#ul@V$PZh%byZb#8j z5UUsWWmXUF&DIutzjkI~f)xfhKNs2epCGW&(9n=1%GDp!?S_W77`Vy+lwx9H0y)tL z5x6OVq$!D{!Y8>SA=|C}lRRKZDE(&=NvyN|;s#zwVDn%SO-v<$Hxu~0(LYVW-wX{4 zCC2nM1bK~QWtAB-f-cpc+)v?@DO^)RQ@9Sbc{MI&3fGLIDSR2=Ekw~2u9-zscnILP zag|Ln{AjpPJS|ZhR1c;QuOZ@j zTuO-QDHI|P9bGyv3Q=7{Ax;H&60Z4WK=tI~-HQ1LEalApOrTqFm9L{G+tDWK(uuwT zs69k)!q+~~&DKXALyFG>vme*sDipm+&abSi+KA@C-y!odK< z;4dLrmubY(5BQmGjC?>IuY=}YB>6RyJqtht{G8Z@Menq)0Xt$z>w&^#puN96$cM2G4F3dB{4v1jkNG)yGz~(t<}7e}Z62 z0nNSwia@jNAuv)37z>)wxRe4;LSQnk1O+shT%;dB_o)7MMgB-0RKP4Gsbn&(06!-x zAPdpH0s^~?2$XoFj)2tT7n>~Jfo?$??_`nBXw1c0T2*H+X3=c$iE=&qDj{gayTPd} z&7e6#r0`hW(o{1?ELqxHy`>wU+3Rg;Un^iT1HV8CJf@z|Qp?#5y`uIuER$h^w!Yn; z@;vWRmA?U5P7}7FV}as%Z?Xbcs23QY_o_%Byk~Gv;#6%ugpcdc;atLQ6}Xp}xX0T$iyAgz`8u)b#ial__k@P=}$8B&HW;hRUFxp)$l7s`|wY zmA{;!s$a}d`O6t9fAtKNAw5G?xY!JpkeQ(>5;a3*C^kbS5SyVAh|N$5aE7WVI78I| z%usazGgL)_8LCELhN=*pp=u0gs0`^DDnqduDgiY^WguaOO35bx_Yx z8TMzW4EuA^VFcncR6_9?Dj_{XWyqhQ>adxi#v|g>(YZcF992V9(hQXd&QR5_o}n_> zcZO;+Wx@=VaBPN3z@MQiLNh~UIAMlLFg`;~fMPRL;u2@54&7jWc38+rv<`-wK~tQK zD^qwaEu0{?EXEpGON&UG3M_DQ6L9Io1rn#LlhTgBrbG6QXLP?EHQu-jZ=5j)ksym1 ziEMv%a*{k~w!rz1s^$KtL6-BNK@-3n6dQy%@anI0*T@T1rlBDyLjOb_TP-pWMF{ zIR&~zel7zcyWY&^gi8gAV$=ElnN%M}=erU@55u(|-2a8n_FwK=y?arb6VbmW_N>9w zo|Qt#>oU&%2R&;j-m@yP(X(367aQDvde;B+tg4Scn#n%!f4^r9FN4hn9NMA1E4qMY z*k|nzm{{Mb*8UO^YV9vVF80NEa)-W`Oq^q^{p}vp)Nia>`rA`z~S0|1>*jt0&gNE*$VKxY+)Kczb&~hfZ5P zmA5Zc zPt%o7Vy?1Gc9nx~B6*HXaFv~7JO|3glp^w5PzMVT${mZFdp$t6nm2|-UQ7HD(%f43 zc5pvdVSm6#_rC__{ssv=wF+l8{?x5&ShH0eijs-o1HV2i9|cGBBOemU)=Rb+2i~#1fC@ zI$C?g8#-dBaQZL7n*#$ZaDTDlgMmX>wn#dLd|R)@d%1;Nu?da`$}+3mlzB*iyR1U? z?WT3*#-WAXrEamqX{fsrT)`jHY4U2C-FrADSvV$9I3`I(mtwUv%;ltvt{$u)9%M$P%*KB~<^q*9iyC+b=*L0& z0ItCA0sI<)pAk3;ARooZz?B(7#+muJ4IrN~Q;1lAOM?7^oX>zrah-v5YBEEvivZN( zQe0Oe(1e;n6DT#!8VxnI!TfgD5jA=9>P?FWc; zSRthxUI@=RKMXnk9Kcy)xKj7lp9#4z6~{*5R=n-o(#g#rR^b5ZVFV`1YqxZCVxch> zwIBF^af3z@@M4Ju71LqCdYIZqY!4aBW*<1Fs}x9emAv>Wp4~j;CrsS0s}#^G!cWXq z3MsKtT%|yNu2LY?RT>raw5#M44qzYW`L2?)J-vjm+Oz0fR@AH_xo6S2yx1k6{U+xM zl`i1>O8m^*PiRqr*{o`(H0!u{lkiAbFpbIF)Nroa7hFn~bJ{R$+zUx5V2 zE3aiq%@R0FE!ad;TaD$gd36m-)P5#W5zT8t&uMYnkiTs@D=)nPxe9AG54=+g)oNE! zw#Ek&@c#LIIeF<+MbyF`>t5_**u1H>P2}aJeG152i`(TkZ=%N}3i2|l!Mo6czlO~U zg*siCh5e6+coT|*frf7 z+PcLKjS2$AGilTzf){99n7B3DG;XKHr2*H|BQDfX2B9voOG8mWtvJq6T%_S_z}WJ8 zv4(P0thhu&`G7jtt)Wo6HKq`l&1Y}gEcR%)7%#IBhjFTq*Kd>5No5wl!Sl!y%EW9pMC(UsW7n zos4n3U}<0zCv_xb*7P{ zSq?>ap*5VMlHAe(2cVJ=6+AX|ZEZ5t8@MjxRS+ESsl~YX+8R>?v9`7W!`y$?)~JF1 zYuDD)cDaP77KCGKYXtnK7K-r0+E+K82gGh!Cn`ea;XlF%UG)SbG%PY6mI@q zL-YiKt~w-5ghr^ysH@H&5}}bQwh&+k_IiazDX zw)Af7TDOUvOK7a(IZ}9hmLx(Gir6ABacfw#`aowOrSaQu8(YqvDCDVj2e=fa&Hn|N^_;U2uPUT)s@0rab!wB43l)eB z&&OdrNytU^-HP#&1rab-qbagmYIrLcRCR{;*=F0btUoUIvyo((&Ar7Fc0+|D<(HyA zB>T;sZQR(`i!+UdyxruX)CKimli7h z=5Xa6aaS5GFl0Y!D2nGN9;zIvDB;MKm2w_(xMsjPK(}FQN03k3gA_7I%HXTG#pv~E z$>DLOjK6}^M@h2%ow)(n;|%b(96+K4)DbGJX-%3B7q&_D3A z7g;@8l&Lu4pg?9K78ksFGqxuR`9-DZ$;a8V z_@@kI)~$pWJ^N_GeAmx<+po74l~?U)R@RQ(q@-gnnQ>Z_5B9K8LCP0bFHF z_j|+5ngdqvOS=)clEb`xpSPEBo64y0TL|5YRJY*@{}I4ov|sME3J0;tj4yhqWs(5^ zz6RjSxWZ!rJdeN=1f~KAVGj5w0Of4}8o>5z*p?G+2e1M$OL2wI1#k%h+i^wSI7OFr zJN7K$t(=g3>!&rsQTzWKYik*{1j6yV5veYnq2nw+06SKH&&ks=v4ZhE(0mhI@5W_6 z1mF<_9wKlTfaef+23K|>D<_pzKuX2uRZzanq~!A!0zV}{K7T;qeO!@ij5zt&G6&hY zT@gAb_oykA)d|cCGVNEsbbN#P?_@--5da}vifb_fg}A0ArgxZL`XUWQ^kC96@dO0M z5uix35U9izDK!}MKAq2`>QfDrjHCHN64APQ-*85%>cz@8eR^+-NvSKY)Qp{q;ykl}ezm`8%kUG!XzH zTuPc^1PXB_@v&3+_@x>O%E3&^QcXZ$908VU76O&HB411?6`L6T(jPE*Hpi(bn`)BV zOp_FmCljJoRFxkk#K|usg9>AR&6=N(f0OoSsH49jav^e_hiit124i1+`}W9YLp}w4 zwIf%l7L9E+Jiox&&5?umulDrD6qIwuSNfxn(kJ7a`Nsdrqj;8=76G-E@$`2fi zX)^tHV3!8JqKz*yO_K#ajE(lj8<}p1&Ysl5N%6_DlzF9fiy5-~T88lAQ4hX`5@j!c z3VXP;&EiZsd>=wX(lEiqk!7MwhB*cjgCQ2D`|C9o@wm+U9VNj08pAqYcNgE$C{#4P zj}U#E5WS_rEiLN6{}zmB_~0#8#HiaS3xjZhDrQf2E5f5Vwncf;e?Xq3hPX=j5m<%C zoP9I)taY@v(FfpS2Zo2AOUd>&v_rh3#)+YDNa&A4)iOg8eT+`wLv@xyCwZGS7mRX48WOoPh>E2>8fY z1H?v^!kC?=uQnNe0fgCl)KLtsFnD?>>0dJdots&TXc^1HK`B@v(i;-^pP_k#?AP~c z9f`==glN@F4x1=PZEeR^W$u&g!kGs80AeAVq`z?5q16BlFGaUH3}2KGO#|q5&=Ywd zS5*#N=PvQd$Zepsu5M2MmRZuT1kq2lFmjSef6<`HCW1=|axgB4oQTrMMR>N7kbbZx zWcuG3v<0?R3e9c#)IcxP{U;MkFH6ie-=~PL=NbgY{6O`{Poyp3B;e5lT(JC@YO7BYFH(r zXIa@V0x9Qpc5FIRbX!G)gq)+m9t{RX@FrjbYQLRM*K4^%9dlD^;-;w#e6e~l9;Vhd z)#6pl7Jmt7Zq1zK^F_t%!MPE1A5tx@&KaDB56l6o#=c^7Yka`nglE_7`YCv|j7@#87Bs4UYF%ph28@(VsNx-T7KA(OB7tqpaIiwJy}C@lbSs-$L50A0W7DJTJysDa@h|VvADi()K|*iXP$h}S+oK-mnJJR%?u02jbpJ{6c=S9 zDrAdibr+l*tvBhHxQohei7TRMPlQkbDgC5|LUw8uO7uLnO18CeM;3;L&WrD@?h0u>@ zP(@z^=1Ps#Cp?JrGO5F17#`Ab4g7{-zesZzzvI*uHPt2d3}*#h1ER}u;o1M5aYTFx zz^%BvADEQXT-sj@@DTv|y$)l0SS6sm7nykJ2TDwSa$jH!;&X4s&AZl6&BlNx%9!q; z!C3oP^os*$09VeiPLOBL22%-y=Nga}k~Yo&tp5~|;8Dhm9SdU0;Axp~3R|Uo6nW$+ z6|^h+9g?Tc-W3}&s5!Ob(Xw^wlmr&=OYu^#Z{K3}W1u=TQ`g%COoBhT|2#})LLd^e zN3v?}gA(tgW_gSO6N_q<-J8I@OguU^g`TU>S@0M_xYA!YjA?wQaeky;6BJ&KQI)EX zQUlP04FPQ|S#J2NaiK?3=qkf|vl5v?bAF_R9zBdgPlp8KDT!+AqbIVlcLM$>t}y*n zmSb!lQ{4{K=W(TPHcZ(VG7$lJ_%7ixxj>{}kx-ZsI*txi+N}nkD1VSW(NJ22@9cEN z#LTDtJc-<%z%}eH071jS- zQRm+`VxEUK=j+Re8Sio-*vE0v)fjt^yXLm`afXK2gcfeUKIc3-8roOy@rF0+l( z-_!80$$km4w>}I{GYFl%2oWUQSnPK__FB1K1`k}N#F}mT0o&irdAyW3B}b>K@_UbO zV@^XQegdT!^kI}DAIf_ZB}qM1@D!BlIH#}~>B}xbp~7)@dLxyby0yaXcO!+XH&Wmy zwvhsdvHA01eb!rldno!G&Q=}_C-}?Xr9f&Agso3{3j5CUU5rU`o?|CHh4l^zdv8`7 z+O+a1!^!^i@eY5|=QH{KG*zIp(U=EETnnDb9(K|eRIRgP9C(y?)KNBl-86VtIZP6O z!`SCBG@ksghCd_|p8PAv00{rdKVgfodh$=mA!MHX6ZTYW?8!e-L7)kX;W~+W^52CtBLT;s{C^V^l_Hyjj8re<#_VXItiF3 z)=7H;ioTANX^i&kiVxWQ2mHCeDBgq~TrR;W4tTCE-qB^p^Ck6!y>4CX0n9ITjFtIm zRQ#`s(@%wvj*LCcZ#-w?+RdF!YBlE9IyM+*q9S|5Z*)YOY{cZZZe5-Fw!oTBb9U3a zI)`ZHoyg(08p!rbn<(^q#er|4P=0yvuHNjH*mhmE(z>L!fdAg4KAhr%vd&c>exlh2KC+uY@2>}O-U&Rdnk`PT-KH-vmyAg&E42* zaAteAe+W@)iE7Z6T+*>AHLgv^*+wbVJg1=DNNR9#AGuBK$6lwSaWIk6<8n-^d?#8H zV1F;`Cr!@QPwnc~B^*_;K}SXT&|2i;`&%g)??s-nV4Rko85^e!Hswc zSe&Ve%Ov(}x5v?)8a-4i0N-51w}dd?)2Af)n%JndG*WBg7(|;i(P+&{iR7o>tfR*2 z^hr@&I%>R5h4Yzu)cH()%CmGd4t``E^oj1)(UauLj!ly%nMC?E%sPqyi}d4?}Gc(tC{ zppSwR=jbX~kQj$ooOv$QxjL#oE>4%9>f<`9IU&jKx0Zf z4wbl~WH_H84K{-gPE68mu@Q|iDBbHza$jeAY=oiWfIX}x+@Qt9S`jSObTj)(irD2j zGNt?ZbjcvL=E9haN$fvUQu0@dcL8pGA>st$OC{yZx)?jJmckb;?7Q=d?XfU<=M_=0 z?!{kYitoHy2)|PLJFosqT9~fhc~ycuf~tqZb}QY|wHf-Gs<7RPXq=VW&vq-ux=rR>;Z zwE<;Mqb7@;LHVBrFoc7G>kbMk@#?xe)^ii!Jf4@-v|$U@0lh(tiK(-8Nqrp-@t9lF zhy|N^o=uf&4@!fpJai+t2{noZ2PrumiD&j(*L0%=pfTc%pRmSb{Nm1(h=$T&L$S=# zG%o7fd*9sB2L&j$Zz_EWG6q#FfjDWDX&ByJ+-*LYNcSJ|bH5y)F>yH`pB&-IbU5{DBii#}^6Q$Tr^Szx zBmU(6;pv!CcZkCyneV%B&3}n2$mz?&p%qYbY9$&;LRf%GV1^E7`P%g`zwEgcKC z<%k)CD|j`CA1k6c8=@ZcYTdmJD`tzwL#(xlpc;iM|4KYW@}&tiA?69rADu4)?Gd!@ zM#wtC+w-`ENWaraFX%THN=?kLBDP7~1>T*wY$mz@f$g~XS5yhaU50`50}FMecrGKl z8u3?v_%dA1asYNGZu+4FWp>ynw*-xG=?TsqI<1c@v+K(J89kWRB3# zqsQ%BMtXg=*83ilpWJ^AW;|J4O?v0i?Zc?*pMl$taVhP8RcSx3`|LG_=8p6)f+m7w zAzTq^`jFN%Kpt|YRO)9GI2puW9|*M862bmd`mPa7aB;;~DZ_D&45{0#S3{zHEGxd+ZKKg03zcR(T@5U0w2E&|(>h)T+N z_+Wf}jd7c5aJrY+N#idwTzg`I=Hh98R{PS;VrMB^?ryNyg-gl4E8fs9QVne%Vy?%f zD*0kn$u|T198s?#^(9drcl8y3U&f^h?dPaoBe~QR+V;zl(vBwjg;qyk4aZeVU<``(HrGRWZBc#q3eV{2^GrfJ+s?Pw2jl zlxLd)Fx+3Qy=!t2e*}SFYYfK=@CF#ahD#OTeFWaarT+RUK*CP0wXDC@hW`$eMDDk- zZy>8ntR3C~HF>bQzk{7BR$Hv0igwjR#~^kTF4Z1=P9A&I4PV8c5p9nI`zu+OU$W;w z;FpZyc*&-L_hekEWD5|Ohl_t9c5(X*14$F<46AOf2?PzeR2%ddq58JfK%I_DHN^7~ zIFA4u;%)>k#)ZH1mLwaWbB-D*tEr{KW{LN+ijac|1<2~s6hk=Pd6E}c) z$^gEDn8$D_JD;!Y{8@lc6Gb~;pzQn=fG^=v`h3CUY2AbPItTe_eG*L3XAy^tpBnmD zpuh-gbQ@Re&ydBNxRhGoMc~)C@HhNv!{Ta`rxYv;^os9@8Aj6`z!TSl`!Xz_7T`(t zbdV-1PK=|=+AUt(iTdUZYanG;_`@#E1Ji9&cLM*+Fl>J^G&L^&3(1W6;VF&w3_os~oL#{jRxrObB^0+$e=`F;|Ct8n2!HC$Z4 z450abD67C6t2nAwrF`b(d}M3IpNECKR06qQ`~-DFVWvPB=fhY{twyC};Y$66%8OKz zzv6q$m(u#YzyH7d!lN6s)T{UxX5NijvGm&L6$4%Z5PK2dGV7rGvW^m+{0>;sd-$H& zH(=u&dqo?2uPf!X`otIN1DJ&;ERV?abcs4iRUdl$gy(zdLzaLBr_;4+PpxHj$g8tEoAsNj zxa9hE2c*`om)W;|1DSsP^793(-$FF9=!emY_rfXoD6YdPUn;r<_3Q5;%G!wL;J;DL@ z+6=(C?ytdVV%od77rH;ieUb{7!4}|;sqjdgc1%gJMm4?hi3C}jl4V_>gIHySO4ier*HETpJwt^@Dp^0O!WD_zqx|=IQx5OTrbmAi<*?Pw zfhQpX+%*yCMVWKBl%WsN7IJG~IAU>t*$(ah2i`%lpm_ba`4<5pX4ka61f&yK$8=BK!a_Zv*-XuJ988q8NZITmzm1z+-ct z!dhsU2O`V`whEURSTk2w7u6j)-vBEB{8p`-HduJfp7}|G|1pwc<0cLW=GIZPRM{Nd zvOQQfG*~tq%tF65f>PJh4K1m#TXpYt5&EKG!|0WXtaS9fCYtrQITkH4Pd6V}V>09} zD}07xklq8*!d|3%O$Rmwu5anVN$Pk9DJqpuK#9w=u4QxtYbnCjvuvg0NSDY_6;K}O z7DF4I+ef3Z!SxAI82`_rvJm={E>RsHiA>W9(WKBUM}>Aih61NRV7U<7> z@=H?Lhm18jw*pj*T=XQ0k=0FZKBHrP(O~h=sYrqM(R@zov?*i6{eDznNtw?3ph=OZ zMnVT8;d#t^7qd;rUPZMlvRJ{N_9OJWV;Oi`1Y0K06zN}O1S>$Kn{AW@D#4S)(;e^= zDDt%bijn8{|G@)GfAHYYJU#ZTCujW0{r94a534nFF>MOJngsb3t7`L7_j3xCgXSqCSx$t+0xEmCA;S%nnhJ;190@2(QhjW47 zG-#^g^D%TJeZH1-1JM4av?A;WImY)VEdBk7*G&t2MwO1`qj08t`O{gx$05uExWei@ zr~F!4!&`t~!&N-p2!pjG^~Xn<9*3_HQKfVY9s-xgeSduSdZcO>*+6CBiZq$ztZ#nB z;B1h)GNIfTfuAY&sDyI!s8oaAZqPIUzKqGT2g|s3a5|m~C8W{xIMM1@O~KNio4^bn z?uxEqnK?~0r2*86-&+mPMTM>gvcmZ3i2wZkAFTnIF6`ipa{Cnbvlvr8pX(D0Z{Z&NtjcMsE*O&p$A=W<&r9;U{!_ZD#eaTE@X1#Sa35n zUY=g-*X6ia36~e>@|BmcfaA3xVh!K$RJB?jV3uX&Hc2Y7Kh(bhRKoeoe z&_CX(V@V@4oXL4gOeUD3>kttrIvNOZC*=vU#4E83;2rH88G%|8D5j&HlQAb7_E>ly zu8iXp=0R?i4zx2X5~qJ-E38JW2DF?wRUP$>o5)4G^KgrSj)`9XgNP3mlrV41+i*Th8J1ou7qA|r^FlK zLRJt-7cq?xi*vwe!I+)kJ3_kuDc>zQr1_N`Lq8?-NEgwKS`S*P zLoOexZ7AIzA~KSs#dyVrf$B|YA6aod`n^-O?wxY94dwcsMxGg3VtrD+;%*wMuB?J& z)9^>KLe(29%GB0gl$t`@yEZ4h7~&6$x@0A()McuoOUGmU@f!*TxYS<3j7G<($npWP znv5%hM%7PSR#xF@&ET*DJtPPHfnLaD2HIL;8lw5yaa&4g3)(ikxI1Jg*oNab9#@eW zcC;Wb+42H$-a&12p_o~P6Y4zT4_YzXLEBK~t|QoX;y#Ybquj`*JRCi;nlxFdRigWf z?BqItOQ5>uoFt2+LUMg$_3cY#Rh~;B;!cmR;liZ!+USZf7Q*9E9cGBhiep{7>Y!+a zSUlD4QfWVOm8UV)fF#zY*%=p!rPrS4plNC-EsL?y{s0O`9_0}jq8^$C`yL2Vq8l$O zSEJC72!^krNjbsyQD&avQw@)slHV{#uwmND0UoIvKX#nZkr>UD*fDSb68wR&Nu8hR z5yvGVO^-M}*02rLgw&zEBDs5<7}GB}j3~N9Dpz?@%n%gTZ~K#z(TUA_iqeA_SZNC< zB_Xu4Jynm}(-IYK#`)=5+c@}xk>uR0VptP1l)Z!JDG{ak%ET^6_Zl-3QHgTR!WY!R zjBHL#q(MOw$*RB|UJtyR%A!p{-BEG0=+n_B`U!qpPVHOFAmKNLfNpiMK_q#6FrIC~ z=#XSl#&#n{^W_1$62&7RCX1BjxBm2m)^1Edk6NU{P~kh!%mzh`>T;Q>5W640>FcJa z{MSVNWb2VSC)ogeyU5jJQC?C$Xz~gDR6*Y+P^b;Q$RCu7Q*E~-W}69Zzcd-i?qEQ& zR9ZCy;}t(BW^XJK?8=NGNew@SMZ2eSsoT66=d_XO1|J`v&PCqCXt%zF4QDOjwMPCJ z&*^_UjKoz~?)gKR71teyvZP_BuYu~iuoHBT**9H<#M!qlV;{>z?(rEioUWMS{FxFg zMbE#@$c5>5QW|5fbh>mOo9-K~?*k{#&@vK@RSy)Lh(m}3=PGF;-e3QUO#*uK&hf{V zT-^qfEz$^=-=|dNYN#pj+jC)Dsk#tFu@uUH^qd%D6Q|>j5tR+icWE%}#VnS1_%v$T zl+d`$7&5?j1wqeIdtki&6^vVy-I>PT6xThCynKsP?S9HP`0o8b)g7YK$a&N2Um z?M9iaUU^X_%QXQ!VUD|Ln2#q~JBxzJxbL3P{KqKYhMi}EyF?|?P_5*qN;IeKB#wJG zmhK%bJx_pjCs#07JAHM)saS%~WZaiPtRFDi|Nk~e--=Y#vX4z1JG6Z!xr55+W5lWc zV5~*?vr5x7`F(IGJ_029hbab?7H@DENW!dAu8Z{W6c%aR;*uFPNOME zsqX&<#9h3B{%DO=Grs|-g&*Y$mL<0e3=te4`gjWoR;?@FST!r(j#XgXT_{(}KyF`U zdZ|NUAg)$_I*?duJ$Vf$w>Rx8`<_G0T;h!)EwNNaGqL$*QXAFQ8eh(hmBo!)UBaSB z+?mlMjIjYs?c-|kS1A+LL6FQ8q(6&{&vfFth>b1|o%$?Rm^PSC3cf+3!}WdD>mSBjA3e=ygc zC0#qm4SUj+TikFK-o(EaVcWYOp)A*1?*_cZZV6D~b*|+ta)xM)gg4yZrU^3ea+H1TU-LM@( z&Z}Mo4}0!j*F#*uJ1xht-GTPEzbm9Wnb}GEzWcXBIeU@0bkprU=k|hZQ7$F%><`k2 zb?mEq#v^W9FIaaVn&MpN7Tf3SRO0+TCeBV!7u%OYoXB#}*=ge`Q?DBy4{q1GBakw4 zJR){$Q5n2WX(-?=b%#*;8^QWCH}G9IpOBr~>qfkNu5-Uql;f>e89p7$u#yVMv@b-C zj%Vk;r=ouri++#Nd-jz*(#v&Aet__X2Z9p}FMG=o6#rrI9&)n110X;^p5>OD3UCoB zDZ_ab(T(KNj3DCdZF`X^bKlhL**(GWD$5)kZCwgODPCvhX*2n;TpL>p2TD zo#7pF?e*>)ptIO&R^=*H(F>Q`!bod%mVREY$3=8aSv+n2l&IS-kOMM>e0fqc$|dMaN=j^8xu3Txa4^%mSAnB=_>R zk2sz$WZl8ry}Uido6^1ii7R_IwvMrF)a%mOMSB4R$F4%jayvQtlOoTjk z58A$MZ-YvL4o7!@b_OhNJ^CUW4<1#N4NYgTQgd_A#8I1-FkqNHf}_0AZpK0nCRoHq z(i2=rHK&6^D0rjFl4fYCo%bQUh1 zl1CFs9>gqvfhu5i_8couy9$q%yK&uyOQp_y3?cag-rgYYm%Q;HUCY9TKr4;6T;9rg z8_nA!-gvgHRmWR1Z)18$m`_#(Q(9R#+j;3#)`lfeU z@Q1+%5o?zmQM6lfa&qDBL5-W;tl4h(O&IR`y3eh2>sGnb(C=2d<*-J}yTZ-lFZ#ew zfl5{)s|!sPUdO8IUFf2lW|zqh)W*QZ--*pG+Uxi>w`idoJlic=ftb@&yY5f^+I|`85yoIKPTYH8hY-{yH}b zdAxC|Xg4aA4ZYDVp#tr^ohpT=fV91)`(Ny0=*ss#=}xpOdtXEfd%1K6P&#iWn!-9L z?P~Pn1t?a24!GNG9Q{gqsfbzH*NAuxIXP~ICy(675gh%dw7;QbbY;}Fi|vA)$n+%7 zegiH*+Cn$*NsLnka5qBZ-O+Y;r5l~?7TF>E+S@AKT>IQg@SdGh4DZA9f^++kCOalg z2c^mHUz#J7CY{pUnIH`X`Llo$?2ETAM8TR9WRo5=p5@s;1%1GK+0E*51CzWfRri_? z|AvB{=9WL|W-av4MFi|$phP2fx`COfuJG-|OjA0{+lxw}!eBJ?x?-BTRBH+aTjd3J zdiFLUlpSU9?no#emHi?Mez#=Qms6s4vWE8}$_SUh z84=rVJO(~k)=vcN{0{7(hV|Hk=mOepy(rxXG?S6g$7nZe2?GwsPHA7eoi-b{2RL`0 zeFWMIWWjMn){hJH(gC~nKAe&(<;O5cv$tzGW^eZzdZnx|9-ZS-y{KozrIa1b#R75tw0)YcF=AX8}7Q_Zs@@uD#XGn$ZKqqTKarluNU>j8`5(v{GfU z9(}CJW3B5=LI%ZjBkhhCy&Js^deF1Ivt2ZwG*5ZUnG18^dBBy;L`#7kd-j$}RVuj8 zLnpf7`6#g}FN*sSw-k9qyu}zY3$JFc*~?1oY1Uq>XO9l*a;|a%Z$T9pVAp};E+r71 z6WcCC=~*m$O^{8=E}`GJ5ar6%VtecM-bef(n(i5icq-X0lRn!BrWNR{B{~ua@hym7 z30W4w0nTtEAB7WC)D20$qR1oO0q=Gs&xhOv%>VZ0+d0U$ORsmGa+Ut^sLUggFXJ*6 zr*7862+5~-`^PC8aWKS1xOB>IAtayS?NsJgsIT(65cT*z+4DuXU9j9v5h)t)3%tQ;s+(H&2e7;j3Jy5|yzW0|i=!R|_L;apeS$ z^Aa=w2!a6&0-)VjyKuK`uY)c^Evs4yNh?pY$MvEYY#gsh`;`?5cPTkJDB9y>t`cj| zLdxV0W8MpQLXMxtWrF~PXus>45-g-P?QOWkpTG(5+?~SVv9vx;p5J@f??|s3j#N=~jYCgYI z`CM!AK^63CO!l92K6{K(R8_sreEOQu?^WLTyqNMzEcB{9fRZ!4w47GSN#Gjy>j$VGRNx_?{|*J zn?`Vh=KYKF6O4dEZGqy>+W)}koC_n(am!b_gPN5Q1cKi_X5CZHzYFw~Qy}?(T$eq) zywKa@%WP`tX`gqF8Y7&qNpBx;F+8>dX1X4>8o zkiOz)VXAOdQZJBXy$ch20cE|{Qq^{;@;*2A=AqbR3;)9oH_z0g8%9sGmVh?I(U<{X z>KO30D%%Lc?XEzQOw7#YR9X^FLKEjD=t7S@4Z8+i;-l;qRk!^Ygd{fK!YnzPsq)`~ zM^C6MSbCEc-&Vu7@|a!oMPPCdE}pWWcspV2{gipUmEmTMk26zK zZ^4U@ZsCP4=AP-N05S6`#Blm(508nbq;tKiu~IY6{`PiETt7epcse08>Wo?>!}y40 z!25y7avcC(EhaM#Tqq0vJ#ug>f!pOeU+I?u3v+vqFw5~O%Vw^tosA{91vi@RaW{Y8 z^X#eerySOl6LY39r)kHkWV;zl{FmZ-1edB{`!$5*Z+JUOoHkhV<_G$83W^bm>u(to z++vJDH@b3WvuY$Y0|XD1{=PYOL34shj1yd{d1>!4X~a6KZg@tVz~Kf{LqIdCCxSPoER zb{guQp+#=?N;i*DGtKJg1SlCkVfQ*L1!2Ih1h5`Xd6rwe1;P83^ALi~urCimf-?2X znw6DDm~xYJ+3X75RMAC9i6u*>;Hq*620^bRj~?^o2i!t6Pe3~;+X<}8nE^T`*FCiv z>Sh~YA+g?tsd(7NiiPVf!ZLH7y_Rd^u6-7!O6&_=d)!Vp$8Mx?&&13GuZZdOjvVha z6q-xFH0Lz>#;cT(=Xhf!VqpDh>A1J7OjRaK=0AV`f(BsO;wb*GD7Y<}76|S|qu?L$ z>MyF_y@JE9{;oB@QGPlVQFyU0*G0Zu=)+W0#f6?0r`~A-Y1oMIh!NvaBgSJ8gY=IZF`j}L zEQJ;$rnfTl41hxl|9pM=&jv8;sz2iU)BjN5cOV~sNq>eW7h1#$=On-xSO{Hg=qje_ za(~RHQ)Pdpty5MxRiFr)LOg;(knm9y0?bed%ENpQ8h(QS^Y2)4PC?vARJp(W zJwZ|9DW9s@3WwzngFDtKM8tvnqNLXK#dPnQd@k_Q9aHXMij!7;qTE>i2WQIBU#SxE z?UR&QvRss7hxvvPx$*CKb*V8WFJ}-2% z3cH-=kSc7f?-5gRkD7{m%(uSV4S#>3*8g6l;eWB;F0Zn611-DK(SeayxH?dAx!*1? z3g}c-yRexbd@~kY7}CP*mDr7jjf-Br19wY=A5rm-s{3P#%~R_BJ$3)Sx<9RAAIC0E z2TuVK+ev?Rj{^_HHrS58oYe)Ge~MK`^ncclS0Uj)-;7m0tS9F0f7@WQ!Xkfdm&dNB zDhF=BaqEd3BAteR>IX6m|9;x5Zb5yF*TKUW*nNY_v~Mh%AG@R`T&g_glNj#-=AX&$ zLd$Qc6+niKRXq}GuveyFRT-H6SnY%O=@89dz!^(o{Q3CQ1IAe2q)$D}?w`>2`AB-a zcA+{nHAEb{@x*YWNG3M3*)e=)fl?J2s8Ok!_vhH;Anrn>O|0w-sMv1PMam2xMFPI)wSz z;@+M%gnGcHW33|GNbH)f4ggzw&*^IKK>$qKyKy?dB2b+CW(y3y=F87rBGhxvrq&HQ z=?Sav^{uTNam4?>Nrj8JIPp;H=KgdVuacPZPngos^{Q_7-$iTxLRmH zCeWTRRrhxG^la(+cTsdqnphxihW+;pDLqI+yFXznY~^u7IR5HHDls9?gy0D)vg-F+ zHnex*n@c?>vfQZ|r@|*p16@7sTiP~{>*n`pPNV{kGn$JOnNq;B;Wn=0F!;|mss6?V zj0PQ>!376W;zLY&G;Qzh-mqp9hUiV}H3EmFBFrJzAGj;mZt3Xk9oMmG7W|151l3&(;O(96cRfpJYEKreEXbpuCtK^c)rHOM*NJLEZoAX{qmWo-nI`9RE&p$hbK1P=ES~ zPrOfvrj%ncK^?1pqHIZJdEB*=<5=1REXfF8T>m%THJMYz)wc>T=KD(>294=4&i7Xi zR;s7PC`6oShfAw}4MzRgVb%XW)sQMyDy)Qu(oTq>Zp1e=Cv4u-zOAFTMIAzWqV&X! zk87;NpMIVeh-1Y;eBoeczMIobP*2d(O?W z;Xs@s7IL1kkK*iM=cs+ikxkAa=Npb9#qYQ9tpMDfCH`6G1wt=4zjFco&i$(k=&vqb z>~4y&iU%~)Wng-TBO9GNG}C1VG}C38X`^PkOfzlNOqXeH8$Q8JKgU~me7ggko&dUUU?EI2HzjFT0 zpR3)^x%l~c+m2Pkzm4_|pNKy;Rviy(&SoN+8b=zLK3UAa|?c0PsJODF1)U z-}2{W>n+PK_r3Br*c7c%P4R^H%C}X*w<-4u6-_>GQR)@g$^gz^#kn69D7$pNiZJ*| z;k+dOo5}njtPRMwtPdKZ~ ziHh%Yj0CrdV3*R28p!hs`9+rTi}Fj*yUNm8xf(UFdObwsCm$0NIbtZHhE7m*qKSc7 zrZIkX1JkTA303V>^dM!=^`@Gym!D@fe_nn~BVuJG-7-UmidA&We7b~+C0(maSfV{&EHA^0{M!x)?h}L_!uEoy&xQ#c zaMoW23ejoaE7DRsw>V!xg90EeQu%+*1MUT$T;+Vx`y!eRel80Dx<2?^5I@fcuMXj7 zU+5P8916X|pPRy82}^7Vinsb$NSSC1^0VNtCD3@EW54yB`;r^z#RK@Gj~45Q*W7E+tvQc-FYxCDyq6K|6L=a}0Yu#vyfcW?{GB_4&j$g%9DI{MZw7xG1edDCLkEG&Z-eg>`F`+&AP%Cx)=A@b zj$#w}LGbG#M1DPVcPJ8%JmB0NdW85#LXU?+e%1$^$3w3W`%37?p&+m<#R2EX)K(L; z_0@1{JE1!CpYx9MH+FLmu(->ieTcu@`F-HRAdnXZuL*jez509!0!W+n{48LPS=y{ke`%qu?u_) zU%=Dj?tc3g`cJpm4;#e8_H(v2W`EQa&hzpOjv;Tz$2e$z&)ScnS|!2H!}hZ_;+|zP zRyqHit=DtdL$7^AqT6F0x*d*#n`nU*=~IgvuUnjLsg2?>MjGV^+AS>=TB+c*Z)U zvv2mZ$B3x1Z}zhXLS^6VXAgwRzS++n2$emYBAq=DD*I+XdyJC&gWdg-PIV)k)(fee zUEri`_Ncu_hm26K%S$Z~TxvaUf&6)RD_ZX_SpN+KIQb);v)4LEFU2{CUljPDbt@Cz zYCTLykuQt)p4XG80YqPhTjqS3MDX3oQ#&vw_b!a)4H(U@k}$)oBsTHgj7fsCSALrK zPs`VU*QBw#!!*l~MiHX|_R8ysxK7?lf9p>9EopTHoo~sP$qex?lS_P#;M@cX$V38^ z_+|NDg#L>}s!^A>sb3iPsa1%mIvr`>Pnq91hrH(l5b*iHAMk@-2Gl?It_Z;0xdNz+ zk5^vn=ufJB9HSCrpJa2^emP6xyz4yx*m)rESRk52Jp{#iEbt^Fp9HHukxY%#ue|5* zE|>G7_jCNPx*4*!6{;TSeZ^aF^QH5?FVzG6NTnRG3j?1DKz-i`+#9gZ8hOCEHvq|& zi2&1nid~V)QRRBjJvb#}mCE@G#cNqO0 z=Xw0F_uuQ>iq=VnH#*PJ14MVb7jw86!#`qjhT>lUEaDH9-ysn$yYfL5P7?5Ih6A*gkoiP@SV*DCpuQy zgqAl6L){>CNKqAP%PBI{DkfTp^hINJg%*vCS7*gEL`>U}F#m9-)d5wvLuR9+7!uP2 z0Nu`NwyVm)3~pL8q;{HnXLN}^q(gGG-&~p$L#T8?>8%@sBX8YUa;rEXco<#(gPJ4C zAxHVgp}~(E`Jidc_|VtN-8Rs>?dxqo*V{h;q%;t}G4(R1{9N=u=c?4`rc~->I&~vb zUx!aLIoB~Yx+OJ^JLMJN`x0wr1qg1oRkb8dIGl&2yDrAf_T6#$o!#hl!COe6iJFHaiF1|p12wWqgm4g1e0*`rDv`Ld3mJ6vYBTM9SC0z1Xiixb z>ZGb0yt_$&S|b5DI2Xv$Hwne_E6bTN+nK*hq8EG} zJqm#4R)M9JyDjH+th5N{UU|P%izI7iU?gDM_;-G0k< zevZ+^*^LoIIKPbtUw7VeoS$Rla(27BU5FhI9&~SUo!eN}BN!0DFd^vO<2mFpv~w!j?%$AW=Z; ziV^{(3Uo7Rnx>*|`e&elv>loNwV*GP$s`$?%nXxBoAO7a;%-%3ee=`!YkL*;tAm2aL!|aYq5;X0$NZV5Ia@_H6iA~)6G0Fj!Y#*+Nmt2C`TsMFc}(@ zkvK9N@VkmkeDGZynG}yIGI_bpWnAGzCdH$QOwL~}4}Bt!HCEvE=DqjgctgWy~&^@QeOsbzpJXeo*S z_hPB@G!IMB>8(jEb*d|ZPb~FO&BIa$K$+B1!F55eHB~$;^_*i632$a^(36606|t@c z&BIdk+S!H*Ih;)H0#CA+k*WQ<7a?2QbGvpYcRD=# zC`egUC*}I(rD#~)8u>ABx|x=dmuntIUI)q<;KhGVjbDV1)beay_#ZFe1;x&n4izu2 z1I5nhnnEw7wezK>d=C_Aoi5Hppiu0)ls|*gU{LDOBFP??=PXc`8TTgw+iO<#bRs8kF-v=`bh*pd<~-J3#3&D4zml zn?X4M%H;-y-m%+lP>w}sJBGUp6va#GUW|X;i)%TqdAOD@0%bx=Q4bNSD7UWU+AJS?>yl&Ln>_1ZwGTQm<#{S}mHU9L4A zyIc#;L_1bIEOj9$W9PE2a{_gJUh}Zjv1kHgP@%j*E!Vx+8!eiLr7iwX&4JS=q|C=&>AW#KAF$y$C&^RU$E7*frINL`?LSZW6- z4Z2*E!=yG7DJV%T72Li?G!IL?50nA|)}7rfMC3-BE=k=gGY*5|; z%4?DRU7q)X@+*U~AC$KtBfC7m1?BSwWfeN!8{ls@hpz`^uR*yO6jY9c8QfG z{|gGePtE07hi_ylc}4N`fHK`qJmoFCNAs|SFG80!-C0?xPxG+UouFL&GOs;<0j1QS zD5|EG;+(8m-QFqF$~L%K^DuHZD2-{hXCv|#TLGiHEC7>iPW2qZi!2X}5-l=(5>NZds-~@`MC3W97q=-h>e}HFJBR8Q! z-S)-Er(j5_co?}Il*VqhC)m#)(L5~mdr)XVrsDZi_#*wh^5x9KQoBKEcs)xYC_NWj zc$em3sV6{b(^4sqOiV}88m!$b9+nybWl~F>2PyWGEA>^)!%|Oz66;|LgZse?C_ro; zmiiDVjauq4{ERf`TwJb0nun!UHK1K=VqF)2pLyL+S85)X`ZOq0S}M42`jzHksgqv` z3*W@Lz7tdV9sl8Ct#8#lEcGi;W;d`@aJxA3H0EKceo!Vtq&}y4Sn4rQ8WWXutvsE1 zSZWxQ`VgtFXdae&5|mk;SFeN*WXs)p2J^7gCqXH6v4xZ~myrAH>nP{{Cp8aCo&Ty> ztXu190w43bpFX8|Sn3&2k_d@9@Z+Xjpw#wPGY?CB0h9^d57Nv<>RNUt^RU#_piJp< zwFOH3U(Lf(r!Plc=yE}^Z7cr6#age{JS=q|D3e>cT*1+MI+~s0VX0wI>f2cAjUElI zuK&?IEOj;D$HA<8_;-+^PwHX||EPIbDh;<4*0L0aP@apW9@IQ6wdrj5@C_{WN{@_d z;nkXlrEURbGR;!d!MGReI_(_hVX2RRGOeXnKtz`7B_JyvmUq_pRQYk5@ju+*19nLL+u;e+R5UC(G9mRcJ} zJJnJMFVDqNf3JC1>N}vcrC8T>ka>=lW2sIwR>i|o4}voL7M8-K*K@Jd_La=TQa=SH z*2_}CwR}Z0^RU!YpfqSHR7^lEzke0;u++<2z<3Gk`dMCwg#%)?UO0HwQ|Eex)&H?CzKmiiJXQ`+-4 z!o26+fm=3%M3K$+IMUISfq_^U3~T6Z4vu+&;m>QOf0T~dc`DZs*O zG!IMN0m`hl5Q#WI>JiPuQl}yY$2PMrC=HN$hvs3ae+Q*oM=vE}E{*%CVta(@}a{;8fS?bI{UH__iSgH=$x}c?kV?L*O zSn6k>OzVt|rXJvpBz{*sEcG2wx|3|-Kf@KSKKABYP@js2rT!a~8J&BnzLwyxx>#x> z>QnKs)Qv=emCExcuz>wPOFgc6SgIZExL%j53EZ;2zN~pz>Qt0zd@bujR~q1rJ(`E5 z9sy;dlcj>|YyCyc!%`mtWm-oR!~^=T7r|SKho#;P%A_7|oE})N-)SC}dIR8RwA8VX zaxeDb2Q?2%{RWh=H*>jO?26zMOKrj|LGiHE9iU9>m_I8}>ORfGQojMEVIAwDK7}>7 zpI(izs^Vd(G$`F#sst$+QMYIwmii?qlR7qD0Vx?98!%#0JS_D|P-5L|VQ{&=p?O&9 z0ZwZDZy>|hnk0_&Uq7JUbio# zl>i@pO!KhRvD;&Ke=qAg0IBEUue!MA@|uUG?gM2CJ-V_G9ZWzi_g}_5EcFy9(`eyJ z3PT_Jlc}r!a^_*F`#?$RS`KcN=OVfl4@=zwO2b;#g#rfXdKp@h;$f-xffCbFFI1*E z|FSoZL47J7mf8i%Bpj~F6`Z3U(mX76ZZ}G*b-f@^*KL}IrA|yC|DdN)x`Mq?)I2Qp zLr}(bj-rTiFD_Sm5A(3pH$X{tv#wwZ8`I3gQa6G!vyP=+1x2jE{q&ILVX2q)A`bP) zVL7Czgz94X%QO#5eHfIO))kxq{-SwUYD)&WPDj+)fRMWWRr9daOZqS>(9w%32q@P_ zG!IL?v>)|_SR@Tg>Vkck(>yG74=7{ZEOlz2g~tvs4@-4|(zb!6UL9!R7c>t`9g{^5 ztEEndlzXw~->i99>I2jeuV@GQ-mRfcN^RU#_pv>x)Lb{~qXEYB>{oM{k)LJgr zNm>tmU|pZkJS_DLC=EIvQV>WBy9b$vrS1YHsmn#N!5Z98Ck-(VOZ9^?silHz`EJd_ zQpe^HQOGzdHm-!OCHSi@*1A*ku+)Q~&@or9lQgZt?h znun#H0i{h#H3mwZRbU>Lx(F0Hjgd;Tq^{nO;t~#mXX zPhS8}LF2rfA?*K|`vLGILvWq~&twSCt5HQ0T317$u8Y7^2*DWzPg3KY5{UCL@HB?t zd=osgnuoH4dvVzx1<#D;sRx5w@62;_5%s0*!3WPpJeE~T_gRgx@4k}arvbH~DDl|C zK1x&UsMDB-Ku!U2gF)#4rC#fzI(09$Cl8)R&GS;10iT%XgW&mtM&|vQ>K;9~>TZTR z64XTWCHH5x;kTvwclsdrXLcLE^OrlI9nB(k4F0NpX#9)o-v_{h!LZ#rm(-yOwCyg> zU%}IcUn=38IUI|1gNL$LIh>2YvoQo`6g-0=I3EK~Qsa=#?sb&&@m}zZ;&UZ?o&*nu zZBDs@abAgG{&gWZ?chO&Rnb4RWqdpY=hNUp1Fwj45Ii@9;NU$4cG)Y{*V*9N7lN|| zJYymDTmhatLvY>&o;@KrH-KlV0?vw}yUU$g*OI!G&-1QTf@EK}0Ehgn${t*&e*n+D z;HeZF=k8Q~Ryb#X+ij*CW0!;HU5*m@<|~$9|jNE zTnXo!;AsfKc?>*cb0wUMc4J3t2+o(lLpCd%U_bu~Jgp%($6tm1N#g{^b02s%gy4J~ zJj+9H&b%6VH3aAV;AsxQ`5JgG55aj1JZ&L3CtriS8iMl%@N|UWq`*@O!Fd;WwuRt) z20UXSIQN04SK|cN`;*|gJ_P4g*P?%Iq}`dy&*VnBAyVO&w}U95S+gfkH$GYFb*$z560>tIG+U1L5&ma zpI?LL-VmHq->df61In2%+fCE{hui6P@Ei=m`4D(!wLQV*dWLj`;AF4Ic=b6PKf&di z1kY>;&J*Bi2*G*d`!H4y!Fd;W&JMx37d%Um>+q-Sn!}0zs7}eO=x6Yp6oT_{@NCfb zG(e-wV^4vnAq3~*_oKaQoZ$ZAHt?Jsg7XY`Mm0`weOZ_Yw}#++9X!`*oM3xi_5qYV z1ZNj`_G+A9dwvC;WC+fM|Aet<2+l3wxjY2tDe%x!j+N@Y>w}nIgy4J*Ja>oS9DM`k z7a=&8f#*O7&Tqgo7J{?mLl}#O;ehA*5S+{Y87rZ~$ zsKyD7!$aVqv2i7wYd?G5S$NyXQRf!2hYX+IsOZXu@Ibh zgC`k+bIc^b(RWvh=eL7ro5l$)*AKySTnNs|cOWN(;0%CgSqRQu;3XE2 z&dc{>tw7@hw~HIWGaiC-@>j4{5Q6gs@Z1!F)BaVA=RI8TD-X^n#qo{Rl+ z@m*Nk3BkDoJV!sTa{twEcPus)g7bdx)Q8|a37&gHaMJf+3>kuRH+T+);GFz5j3GmC zJ_w$NLvUXBb@V$SIC=2U9Ia9u-UpsGjT2m7XH20^LvXGJ&&Cj(-+^Z-#x|Agx#%0{ zcS3M-;5jJ-=e^+Brg4JX*N?%|5Q1~!H_`8e;B7uCI51XF~|i$G~%)#tHV%bH1(SXqD_40?&8|&i&xIJj9-Jzk?hVf^#c)210O- z|1NS=2+kOIXpUB?Tz>-3ogp~ez88y)h2Y!+o~baL16cnF!TA$-?hV1|{yx@!LU8T_ z&%qF!*Z)Ax87ukcv*39+1m}z&V*Mw?o}0llt8s$k`Nj8Q{U-$H{opyQaf0Ld&)_)_ zf^*q@h>PcQzY}cF55Yro&r1F|_eU7hYMfwuJ^~(^dsf1E3Ovg+POv>!{TS;%AvjNh zry&F!TB_J zIzn(x_$kUBg0l-e+d^=j1`o{{EBWW`_oLs@IAHc%M>(G#08d*8&N)BB*jVEP*Vi@R z=?KC3A$UeZaF+jE&Gjmk>s{cvPU8fZ>)*gbbG=G9s~s7)z z_8|HljT7vjE5So^y-GL_fM=h^2`<;Uzfg0%N;n?_&!ol)wx{m@skvSyoHTgu4#Bwx zJT%v03G?slJm^*j&)-&E|p z>AL=O2Pd{7OS^E-SpD!T?8QJxI+RtTum^u!N^=3lG$<|CpQ0%a@-l%uBao@Z892>! z&(5H<3<%@~f&7p_9uUag0(q%GUM`Ri3gm8qyiFj_yiAwE@kXOSzF#2I+LuxGX1b$l zP+D#h$gKi-S|BF{@>$Oi;+n?R=f-G;7~vjuWe zAb(pRw+iG&fgBUa+XV8IKyDPs%LVeof1^v`#74J3zFQ#ESi&fKbAv!WAdm+H@_vEb zAdr^{v-O&L3-VMoj2329u&xX1oD_bo_et# zd8t4i5XfT!c~l@@Cy?s}a<4!h703mF+#!&c3FK~pToA|u0{P&{es44g^QVKu!weq(E*J$Vq{m6v!I|@-l(kCXhP>@`<^B;Jvn7iTJEj_ zxn3aC?pXt;c}5`LPzCaF0(nLt9~8)A0(o5cd8t6AyV9o3(*k*$K;A2mj~2+&0{MVI zZWhQB0y!p-4+!Kbfqarc-Y1Zsez~vgt7y-F>5csY`SBNN9w(kB1@d8myk8(s3gr6* z@_vE*xIms1$aH#^QHqxR0(nXx&kAJP(P7{;PYC1*fqXz9KPr$X1oB>ie4Rj^7RZML z@?L>FE|3QV@JSvbU z1@f#wJ}8h!1#&?kmjv=*fqcI}E(qiSf!r*R>DWedHZd)b2Ly7rKzF4DF`EG&SCXibN@=XG{Q6TRZ$gKjoQ6O&<$gKkTPJ!Ggke3VOdV$;~kS7K5a)I0+ zkf&bY_wz=9e7ity5Xj2}a!DX31@bF2!y`8t72&li};^ac!r(lR5ECj|1CKt3Rlrvx&c>}=Y+Pat0-kf#Llet|qC zkS7K5s6eKtKa8?B)B8ybO3Up6dA~p|3FJwEOm7h}k?AyQ(;HI)xge1D3FHZZ+%1p~ z2;@P5JRy+x3glA+^0Yu65XgH4GQH!)^u|H#YB0TVP$2gTuy3*<3@JSvcBFQ0?#oFh-`TIzJ;{&adMmCdb4W1ZW*Uh{9**=TET z2t2vCu%gjsPU2s5G_+WQ>s&}RzM4k4kg_S8h=N+C-~Bs7h{Li-c`PM`G5m_I z2a^0ILuC)){1|i;z;p4N47EMJ6Y7bT-()z3&o*-1(L^>be!ET#Hp8*dg&Rh`i{Gwu z2rearC3ToGc-|4s?2n@!sFpE61@sTMdzE-speQx0M9DP01BONPPK+k1U&Q!1m8txm(z*m80>-P)zF2|@m+K-IrY?- zNRqBVduX@MI6zdrpXEb9T{v<0MjeN_RB5=BDwfAc4H-4hh_}@H9 z5?t@&_^j%kJRmQsi{oFxb@2++g)BSO#sAGuA7fFPKB7q#~+XsW%R>iboFrS?7!9M#^} zOJl%B?R~b2_Wn=0y{EIeES8coZj0ykHEy@KGHu6sNooN)p|WE8n#Av_ec^-eQte{B z9UE@DFqs^lwM4DV)4shSoM$b?PB|R>6r=cg{%bjYu7sZ{a?Ok4XBWsSep2R-&Nqq; zDtlO8@C2H#>I+^6Zpt9$MfLtu@ThwKlli0bjq1Gt1yebpe4qO%UGGELT>ma>ATwCV z6o*$3%)-^XEEDM?KVJ<~)%a%*C}tib9`f@+?PuFeA{!U^c^Z^3KgSV-VSfHDc$A-y zD%-x2RrvX5+RugI%t&wkoML7$pLW~Wm(ioC_SF#BcBp{g1y5|b>h-8zu@@VZZ{vTv zHN{E;8EZI`9zLKc#B&Hdh1V!4I0n~I{~n7?;eRDXl;c5ZLlRQa0{40{o05se-Uyy# z2+t1i6e{q@h&mTl{Aw~mUvufHryymQ!hVashOj^Idz&ZFv{kt-6E(=aIn7Ea*3?MY zhf`3gtr0jBx0Octbk`oi0G@8>BFoH+B7Di&9N|}BSIs9)27IFke-3o1O!9M8(LOHf zi9QHpR8LfZ>2h>RtzZ^%m3pGrfrt7++)Rhfm-@7*uQWQ{fug9E@?X$O+~!5S)gF*k zZw2E6F3MU5&Q-G*d?z8i5k4L{p&;}6sni+u(> z6y|pP*iv5xWd@&I%6C9%JIC90ehtbYgXhnnOd32V1EdY9NzHsY=9N9<=N^>uG+?;b zTw;C(Vzz~K8(jeB6UQ^H(uBoxmyYK~NK=%U7mch&QQIn>r|^yPf_YIq53S>PegueX zO$L0Uczy`*DxTNlo4hEVmmq{yJpVF~hvK;bR)od#`v9-v8C&=QE{f+j!9OaVbNEJ| z%!^`m5T#MEdKI+Fi(+-Mida1XZd0o(h}EIAcT%f+*tWm5LlLnd^~l(uEM#6hb7_OT ziVgF3{zmn+t%K|9Z6H&Wm>1R8sc?y^FNATxMRk$Xb5a8j6`e~y?}etv` zqfsJnn1O%(*2#W>nh-nSDKL*MW`QzgP)eY5!!7Rm;RisOF({t_r6KO+xf_%TgK{rP z8I=D3rO?DuGB*AK7npq*x52Skcx+I+DAxu#KaZm>RDM1ih~~vBiu6tcYUxYr2JtT* z`0-topXn|zng)2b;TzSic~KnRyouZUH;@*8Xfog%#o@IG9)u8Z-h*%QqWnB_9Ovi5 zfnySi)p1w>HsDY^ODGy|+=^dRewMb7X387q>UdrcH^EuHi^dxt0PThNyQHqm_pAC! zIs9Q5qhkJbuuNVQ^KAeLi}`DH%ww6v>MO(>-2n%_7SRw&Z7a*iyu%XHJZ|QVj*TqV6p{9Cg$m2WP+MqH0vsog?%R zfg)-uB%&x<-g+Wuzip6`JsL&AE1*b4!k+?lQ6v;9MS`=tCdu{1&!4W|?>?ugo>5&j z1_vTw5LM1Is?DSLW>8x8;HyJ%kh>YVu1Q!JStK~RCbTZ0O0S}i;U_o+kr}|_^n-4$ zYmiK!kXI)vdDU6@zY(rbW2D}}!A!1H-0ik60zURvz_D#^2)qNuIovImjJUb@kow%P z96nV=YkInh)-+W`YkH)IThs3x%S?Au4sX`2sc8P_e52Mh3+YPvnOf7&Pvq9r4@rtq z^P<-DLi87^HJuJbc~QMjS5fa@^47cW&f|u`!A*2KaI%VeS3DG}3t#WwuA<(TVoV#B zMQ5w1_n-7~z0Z`b_fJ((?+vJruzG(3I%QNVe0T-^HOD_x!&L9hNc*bZUt6}`i6N{% zeL&Z{h5LNw5RwR_Z>oxVS3G3%{jdiP^<1*OQ!JZi^Sr5=_I^OuJN13$#bdUUs;Kt{ zn4x;l34p+HgwteH{1=Jj>8dRqy#nvrrBwmhWOm@T-cro8cK9Y4PXH${?p(U|SGAR#Tlo-@oJoo^IlZvlgfv9|@&IH5oa^#_tNKbY)`_}M zF?L!Y55?FR{#J@Hil6JT8mjV79yn%OCSZz@YyNcNODAJo6Z{%~C(QL^o zrFsTEcf4MM*qEvi8)VNq@XRm|5n`8vGKu9>bKOtjP;6X^;4$l!<7WcWC?0s0(ZBfX zUUSJ&Gdg}6Tma`2$Isb1ey9z~JWnJ1dXQC)T8eM7Po?;ICdct}8gkTfp^xH+p6*of z^9$g}i{fWQ$Is^^11JmQH(vMW9&X~%d%6BdLDjJIf^_W zFN(wGLXnEYb%Eug95r4k4xMp$!)ldLFjH{upH4v`glX>JjInj!#IuRjM~i)5GZ<`8 zWVthrgZO4pTIijY4#m;Fye92Sh4w>2`;5?jiO}9yllJ3;_F19*kkC%=y)?_-T9fvA zq5ZJXJ}b26g!Ztde`&B~w#+tM*6WW&w?bJOQ_O!f1XiwIp{S=|S zUTCLWWoo}(Xx~_OpfdMxlMV&^{@&kJhBUQD|=!+8c%TuL|vBHEC}a+S`QoR-ygdLi?VY zw6_ZF8-?~Zq5Vff`*=;-UoW&Lh4zg?JG~FqjJmxwX>SwSw+Zb@q5aoF`$SFJHwf+B zLOZSZo4M{$p?zOX+BXXA144VZ(Eb;peX=I)9YQA%F7Er`cIiVNaV>&8vXp};Oh zojr~<)bUEPYcs$9&||Yx_9rWpooBU+SgvLo!K!l6zvjiI80A_H+IQ-_`!UI}QHN=c z?-J#j(};Rjtl>|)k)X6UAAm-?6g&=ZX%#VlfTLwj+8-6#4+!m3Li?+Q_UW3m9}?Q9 zg!cVH`!b>ZU`^T|7TWgOYSR9Y(7sP- zpAg#V{i~+-*_yN;6xt_*_Ps*;y+ZrpnzY|9wC@$#$A$Jgh4xr$Ei>q8p`G?oJ6?6# z`yQeFCRe+YX`hRJC#;uoak!W9=yIaBo=XwbJNQ=yf_Uc8%TV89m$X7JW()-@QwGtBy`!+yZdS5^f_2!&g6Po zp!h4?O~yA`2@Je{x`_2oHvPS}2Wv!i=xSk^|GEJ@w8%!kE(v%Vj`(i@PZDK^8^L3* z$PwAN{slbacG-!C>O$(G^|7f+>toJSEwuH+zL#Wc_&ShhSxQruV)5DJp?B$+lmyUE!nwiIEZ(cu zJSL>}ph69))pv6%(Apc}QiEqzt6$gZ)>rs=nn(S}tHuCg;H*BRrSP;@nx6Ke-_gZ8 zcP<`8y*Iu}wI*5{#crryBMuEp4=8P?dwH$`MZJ-=!NK`QP#O%WpMo-t+pO(Tl(S5VY@Zn?fr{vGP; z7{;;iyac9bu{L|W!91sdN4*7tDVKsm7t)mDjXqGOp2Ilw+j~HnbaIQ*{u$!YWk(zi z(sPJZ3O5e#0nauA=efU!pN(>z1qzSH6mlz3oUElN9iZ$rqza(yGbkScWy+vD3CbQr z`x_1+S`5m2Krw6QE1+~EZ#?r1d*dgd(9Io}@}HoL8oEyZ4@83YJbCQ>pwLAnP^I`J zC=EtkOxipR9^@Z@0vHeFcc3f><#`(CFep5_K&+DIdl6BZ$HKO03w`TSP~18_=|2%a zMh(6el%(N}0w_GsRW@G>3X1O0{(evz45^1e;X4{i>M>BJ4gZ|DyQKK?3Q`#?Et;QS7hNv(Y-Gn6i%F{?Gb21n|YM*(28%CkUG zZ|z{7R!|0B$urC{*<9+Z6s<#AA`X;Xb16&p$oqFo|>bR<}`MRHSD ztl*SMmFv{UVzC*c>|LPnj8*Y;gTiY~iZTSsA)|I~Bpkz@2SDN8Q%OAr%D9n{mi-ru z!38MCW3L8f+Q7+xGT`*)3TF>V87*Z3l!JynKLCa2O-kyQpzJerJpsycqwGih0d;EN zyb_fCM!mm|D29bwK^ZgZYX>M3hOX;C;Z+D_&j&%-2p+e^eG-&eL;L-pEH^y%Bq-fF z>L4C_*(_`};<*Ksm|^oZpwt`4-v?!%!Sj1i_Gsi(Prg_J%r+fAq-*7W$Mo}PsuXVq zMZE!ov!pcL)c8ia)4#%fjwL%OVAyjPxjYT*`2%SjrE=X4@W)mbyQ} z^F|GB0flG2%EDey_8L5&0cDS2;a7;qpgagl!wbA^@Wel%-nDJ6T6ff6%tegevX_68xZd>sGOd%e*PN-v0ns*qElEHiL+gEDiR*Pi!) z!l$Ji6-zrgq1&j_JHXS8AaqM{7byD;kNpIc2E)R0{tO=)lr^AiG$@yXqTT`6;Mn{D z;xUjv3JSj$OO@-7B=sV%J@tQqlo7p`fx@efN@_PKJYH3laZtJqsh@%}YP6>R0%hV= z-rV~XDD^rLItMeE0z7`uh`JR|AnFXM9#Cct&%X;4wI`X&egh~CMt$7|%9K&3Uj{|J zH=d;)0%h7jo+Tb_A;e=V4#Vc-y&k&&6uuv)ym2WgV@Gp~BdI~6Afzw|acbu(P!8$Z zv3CHrG{-is_WI{e@SFq*RJ0@YBT#sLp>#bA%5uZP*prAuC_;5OQpbU^OxFcQ7g?HL zG~jnPAD#oA8KYcDQ1)vnFvqS0Wm?-zl1W&6G8&85VW#An5 zKZqYlQJGY^&Hx3C(u30s$~Gest_DS&gV5ke{S=gO1NkLS0ojnsfHGlZiFbn1Xhh2d zD7+S@?70h++hLFE)$bCILHQ*pbh%~!11Pk|)vdwjJq133;#tKqQ{Q4F59gK|ihop9~~W!j*~x$sZH zLoRj8^*c}ojM!L;jB(h|wH1^$gJ(Y|yk@TI{U9ilhIToI{4;pA89a4$b+HM({Gx7@9uEH^y%K~Tnxh`JM$DI?n)0HxdL(f$NV!O(RSUO#X& z>~TFNM{zF!55Eaqc_RS|U2YqE3n&9dNk0L~Qp4td2jzglvjmHnV+LhCD2I)@C=m}# zbalN0lsy{Rp4nZ6mI8a+EO7^T#yvck=s6=avRjqnB}c=nhV~txv>G1!2q=?=pML;~ zy9PpN2Z_f)wP%`Hn&X?9_6hJD)b^0ns$*cGQPLhz8jbk*6evp#d%gwAe#09-24zM^ z3%rpg?R2@`co;lnkz31u1!bebbHa1#VoN}Ad0r06a-+R32W8B#xet^n!=6!4CV)de zR(`$#ls!gH_zEb844kFUt@CBAQ$gVq+!W45pipPwM#4>?956g~Cs7Q0zE2dx^UueI zt_D~KC|GF6Pa3@yzPTPd9XwQn?hLvSlxYKbJt&6_N+&4GjrzJ0lmWxScY<=O){YOc zn?RW{a6Si0x8bqxfU?c-*a_HB+6IHE&Xj-71SN@Yu7BPLO2Oz0E(V3q5mQn-L1{H= zd5okCn{NbVxz^sdtB5!GpszHfJ`0}iW4zkG0*aZV4uLXllVP^X5} zC7?7Ilz#zb(ulfmg3@SYl79oGd!=^-`v@phb`_aa=DOoSF>3IYpiCP$n?NyF7%Wig z4W4&^k~IAMHBgorWxVFG$`8)*7tztpn?2VP$mq@l6ussfqXS6hYX$@L76b( z=N?cF8d47ukG6-jKMBf|Q9Dat2tOO1e-$Wu3~ywK$I$g|P^Pp!7_Z`CMT~@v_H`S0 z_8O6S0F+rn`vag%8_0)1X)tU)`bDU(R&Uh(4Jc-0wu7=)+e0ONGbm<^y$uv|w*FaA zmgixMVBEpSwoe#>iQLals*$6A#{M-jho7P1Z4ug_3O8Op9 zCUsrx#KY3rT&Zu;u;=r{W8mBi%8X&puR)0!HaDDvk{YpbHYigD&vsBobxY|hW-?^u zfT4X4cy2h+n=`)%$|U$y2KY92_a!Je0xH${%_F@gGn&p6O4)pFeG^OhX#U2$z3Y(#|Q%`C*o2FEW%{c988Je9b#AhOr z-0iq|z92s9p_pzP1X#HS)vU})j4JQ3YTqxNrH)p#$-JG!QP7rcsX{iD>xjqKt=(W< zvbN*m#O5stg3!OL-mne{KC!kdVQtxJZAm6J8^1R-nZI9>*m~*K_O67%v_|t7WP%oe z@kUTLZ?QVxw0XV39VboaUEI-8=BEv7H=m!_y5-_dQ>t0PbtSfL>S*umGJjl&AGapD zw!X>g*s}gY^S2iKwr(w`n>SeJcdYI7;d*~wzcI1ZhrG%o-Pv{VIun~5!r1MbHzhW0 z+4?4vqjkeNpsv5r>fE>m_d2__BC!1Rq8MzKh~Jyy9)L@&^B^ya?^SZC)!DVS%U4JZ za$fs3L%j{ZWm9tP`Yx)ht%=PSZ(5hwYVgES%JmMkZ_xT6Q%K0$^3AMS|xg?R4HFvA6E&X;_td4mE+nFf@1&f zN6B32qzk`I2`5cD+A8z;QFD~NDG7z3R_4>F7dl3?Zb;#&VckB~5Wu*V|oj`B>E@z9tk_G*iVK;^X~3QJdZ zp_0zerAFs$) z1*)%!NH}7sjN?y>21Z~`G_5Wa67~Q|hlaQ4_K$x3I#(L1$|Y3E3p9)MG zkF%X!QLB0|Kgy|&C-{e8xs{Z0?NP5G<&d?E18NUcoYeLbvvda;KB05&TMZ+@({-w z;-I-@YS0?W3=gOJGjYkOdU0iXZ>EnX%UF!e7XvvJb1MPovV#RbkMN8(!$GDp(FDgY zcp)Yy$qo7Qq;JuDN0Xh+R&uA)JUZ6U_-RvD`z9Kkbzl^YuQbX^uI<`LlvaB7gT?^% z&nwgR(v8*IQ%IEt+)U8aI%-c9tU)aIW4$Gp%a^QtA(OM{Q4J2qD!80UjfJULWo~6{ zF{3g%X{jZh!xi|`!;3Xg+a97;Hy}*#nygFk_R)ONBLB44q(9Z(xyni;T&V0&VUU~S z!mH?rH(p5z!ye4kRDfnHn}rMQ>WF*VyFVs_g1Mu4b9+a!Do(Rjywz^?V)G{gwQ1FP z-Wc?<_%w_{rARuIVLO^yyb!KuY#j?z^X}q=8w2wWCmYmbZ_7?QPUjV(+=x3%5u$Z8 ztu$)Qb$NAlrn-)yeDBC$My);TQm?@RWNt*Q)A-g1&GobD7#Vl_W|zOkLdTR(wGU>~ z7i2L5?`pJ?=XF`h3%lZ0GTFJ(+ED0fvh0PwRaSC$=Nc=qIUctl|*6-iV$xpW{49vfckK^CzsB& z!{BCn`G1QV8bpWXsgSvb8`musJ|c(^jiN1bW?-%DIhu;32IDJ{c8VjzbW0QWH_`c8 zYL=uq5((~9YnGjtc5b@0Xg3l!hlnm&HD$Lak@m=OUgGS5&QdN@{osb3F&r z^;jg6NbbL)DfvQpb4_7ncmT^o=^eQJv?@ZM`hh*3jc^@7H2nJid>jx!B{P)e*)D14eA8_l8C^El?vyY^i$-B^o>Z_g>r;Aq+v2wRCH) zUC6xX8u2gIlM?o*)rw=Llgk(JoVzHSfc9?fm+nZL?3FPxm9PB)Z+elVEXW!Sy z2Aa;eulJd|xVE8|HK^v{U8w>VBWhnn@*vF1666?Xsk5fFEEZ=QI!l==v02z#F>wnU zCTK^SmF;WaHeAF;Wwm`C+uv!u746L4L6PW5;I@>r_qzO01bG|7Vk4*1>NyQPXc0Zp znV_nDcG%&1B~E2_)~2w_05^5=#oZOwX?!K5?bo!_wMt_+&kewTxJl#(rUVpX}mR?D~#ZzB6h`s^`lW5^eppJbp37=PKvlco`*@1^waav z&v9a>08FVeyHsCCTf%ExjO1lRl} zCGFZ=2voKhxiX3hr#-)g!Bje9uZ6c?U~hWF$bA*INNwq|XAz5|$SqfmcYe_9cv!d@ zRZxC}UFjYcW6a+gdia8GEYxZZW4P8t+9p=o>BOE@V#JQMm{k@(1RG(?3)HeeC0fn< ze(5fQMH{5`z12uPS4Pj`m5T3AE_A6#50^@>uFO9n*o)}lTa?zRsjjo$vY2YQ%u!|8 zW;8W>E0*&V{1$W+=-?{te>`HUb=f^@#{oAwBD`yWRTd;mb!TnymFn^?-ra*{+DN|G zHxO7UmlvaxUMxMNXkQ>L+autD7hqQPX8W8CglheY)~M`_ob!fYTS#|{(-Yyb4QKVo zUgJ!xTKS#;O4T;^+f9=ud+vvMpFRDr7&!n7lHSYujq)c5rQzGfYRf z+Wmys)lQo-)V6lp?nO|;Wh&cm;fJ~2V8wF(8v8LA`$jrq%9)Js+=}ugyX*;hbwjYD_O&tVu_a?c6#rv1r0ml81$-%MAYjOk(m^+uP|)GCPk-njl%g1 z=&>`pMRIRH70rsaTGv50?zJ~?tXNz4#~XXqTe)cs#BOSyb&!SEqf14jJKgKg`$5zA zN?KLbBWh=^vA`;HS>``gr)RI#v@ps6oJRCm8l}_ax)9JeS+pC%UX4P0skf9wTM^Z% zf z4mgiVH3q0HcuQz+e#lCt(|DVOam!!1d4YAfrCEuI6drZldLin#iT>F7C6y)SHbY&N zxpE{mNOLsx9>+ONZw=wv^tYg8B1yWbF>oFqG);CTwPrM`2>XiGpc(8!D&TT-mR91W zra^V?r?Lc%fFta-=oY8GBe4K?BYgN=3N$<9#4_{38fS%iN@%y5g~aDlsJd|OV_K<^ z(e{p>M$9Vc0qJ?%+f>_-);Ko;0`J`H$1Bqi=#-FE7Gx}UMR9tVT6NH4ZJXRqcu2htJAwyRL4&5Xrw(XG(}p(Wdr+NaUQ)~ zYIpxfjr*CZcqxrN$Xp~vT&}szTB+@%@U~@s<2|*Qswa+yu+~2}gibn}fz;3WO&@D8 ze9*F5I~5?B4Cc;S?bYgq0oHcI>?mj4deTXwDV}&PpaM@hD%~ZFt0`O@FNspKQ}=dd zZNqN7xeBW}Yv{!~YJ7FXw6h0Lk$8p*FYqp6xhvmS+s#2t=$v72q;Rf{?tRr-hVXb> zn7x_SyCb-E9JVT|nVxL8QOqB`s8L(oY;W)2G!uojjL4QEjG8Px+p2YIfHSqF*&Dsd ziW(kG*zig#G`)w6Ezjo3kt8x5M6hIbc5ZPyhn(GC?XH}!L|L7)j_N7 zs&*$YbnXjBWV{cWZHMWN)_OV)%HEn#OX9i!i-_Jux5yzqwxZqQ;TY@@wC|$UG_h1` zx4Ml+1n034k8|@%j+L_Fwe{s;*=3=Xc*=vxy)I9_=L&ipy|!5bZSQc_yC`6iw53X? z%LUiMCE9Gh*4qExbmvLZYPhOJY*hE5Li7@seobjtJ3rIDDnYNZ#{oO)=r_DAy|esJ zS}MMJgYydcjy$%Ql!XZ4bMaO(p1{&PO^wX6K3^=36iR|sTOQ7;QR6N4=?ckI5nGAM zBUWZ}(VDi7~@GWp2F~bZdJvhQ7F6T*3imdQT$0;l4a>0;}6L zc&!S3Z_H*r_}WYH^2Txq#QpVxbFsFpTiXH6BRy8T<$1OGigY@*Vw5KTVJ88uC{)G8 zSTsg3UTWY!5m4iWb5_9gp8UrK1^{~5Dxk@;|0-Zg<_gHhL4XxN4ZaF^K_>xLfDK}= zgdTVvU?u!OfeISXJk|B34&fDcn4_B zSOIn!Cj$m4GZ{33^BNa%5E-^ac|BK|Efh_?)PTLK6OjM(GsWgd)5it*+3h1y+_E}0 zhOqn5^|?URp@*<*bUynh;v?DI(k_$&T>`Fh@fLADOvtq*oYjxI07tY5a~48}zZ?~g zY7_QU;;oEp?@#n3p=#fWjFF#cbH=f8^8gkWFVM+qq5un|N0i4nOw4hVKEf@)uij(N z1bY?e_DOA`w9*QjuQFblbZcM*=h}dy-9n10PI&X%qm!_qGbCXL@cMCe`VHXq`>qnN z#^p3>9{J{$xy(@YZSDt? zc}8|_VAlept(p6iiO520edkJPzt%8X4T778b$omfpD#5Z(*%~sF87?Ka_3BWHCMqM zhz+^&r%ic+f$k(@eb7GlDny3$=zTSMUM{A5)w~54FLl)gjS!NptRbMd*0BE>Tcz-T zkG3xoFm0JLj+@hAO1>Vlc8GadVP<%)|2Y-J%;Yse*h5io_{tSzt|WnQFWBst%&^7(6eS`CDU7l+l`N$mV=5)x51yFIMHISMwVN70mptiN-g+ zoF3WqjZc+w-4)3bNys;h?0PT~WMBUnjUyJsyW)JQhi+ zH}^&0YL*7^l%kH|#n8p&@~p7eaMD#!_MY=`TkeWNElYgfkWqqssdz^#chlPW*bRCnPBXOP(S_uo-eV$}WmJZy3sSPd(fzTw5Z%;318h5Bfo`Ib9YqQs5fUb*fR zjlE*cDT?(O%pp6i;DfhxYC=^mv?^~c=tZe{uWuo8crllIV+zX!QQ#>-6wWGt5LeXI zQbCiaOc6S*>_J%zQ%7ahap}Ad%-8fJC%sY1cQBW(qy+(I%cE+TAqRBn!Yr0Pw|uqU zkkf~3xiK|wsQFu^58Il*RT|2*D$2wb4?a@MwR;hiEl|nitB~XXBf1_U;LS^7mu!@| zHK269`?!(TTsd`IcY1|p{zHiB5A@Rc>d0v4vPYFI=X|i2F8`6UK>NV6hK^OpT)sGz z8r1KbuJM%wdNk_1oWPC@+9+fn@a2~N2dBf2&&#>rl;{fnWE%T~4@#1C`5L!7AY zATb?pM?$x!+>vD7=uYqA`iEDZWtK(kGkp$N#3k0a9($v^+Yr#tMYDpBnZJYWwL8K$ zz#1?3b`Kg`sAgDBoh}`{P*IN}=DnOR4;*s~w&0p{nWAa4YP#whPDPOnTwssIQC5On z@&URyfXuC#XhWb%?Ob*!N7J?6W@$xs5ScF9!k~L>58NqH;{jrvf?;%&upVm{`p_^> z2^LzFTdIJg$Xq)Deq_nH&7jS*##q68Flw{IbL?1cMtA_U1&Fu^`#ATV&jJ1PLh9l2 z1RY@JNeS|FGk4z}JP~1c=s##o*Vw|Uas9BM2aa)R7WUjUU)?O=v142wnhGuG`DR={ zq$rwkYuEv2I{Ft;r7k5#eo>5@>_Lq0pfk5z3#mkx#JykfEIIJZE&sF6H0`yBs=v~~ zXRaR>Lz$;!QTCg@_v+iTLmU|4E!$$K)1_OyyHxf;Xl^+dTk#PnvHymQ+k6-JnK5}c zCz>9}SZPoPqq!9tSw9z~#jU+)M^$AGNOMaS@rp({eY$*+7SCPjGKW3RDP;3r3Ucum z#{Ph9J8FR7J21_yu1MWiwR$eSyVIq{ZEn>aoTiJrSo*kpbDIg9XQ1f_q!v~6eQ6Uz z+c7r??_p@#jM^cz6;zUF6019#HXgZM7<_F%;G>+W>(gq~!D>26X*#lydOYPhaz~tb z;_Ap5!JdG_qdRC#*JHG(uC4CJxB z!Xt8t24;&9UkiH>n^azG;eT4)Tas^x*Q0! zh`Mdv<=F?cEkJ$v&uQZ_pa{_()TV8Z9@%4abO*M1?TrxCW9x#Ssm3u6k1x0`Pr0xP zv9JfYd8=mOm2t_cI--qTUKMk-$o5ff)gs%czgfL`tA8-xgH8CorF?NXh8O&5O4_D4 z2fVE)X8H=H;tI=J-?e3{)zRMBWmz$OfLNxNM0e+`(%`TS<@riXe2$ogS--W*+SCQE z(qJ*uPs;JRm?`9oIO&?sg^LZPiaV@f9P39%k?ksGOR1j045*`>iUwl~ne58e7OSYv zPUC2T?5@30{b}KxGw5~|UtV>dZSsb*MX{VB` zOATjY)_K`nZ!+Qd-S>6f?#-zo@F+{I^D`xf@4S3*tvX>j;K%iO)R_+{F`9vT03;J0 z-R;Ah^Es~m_T2iBVlk5|*$f&tlN%Ybuq&B@XE57SOci%qIaoCu+b}qo$d!t_lld&^ zvr=iWWb-{%-$)M1-1xVCD`+KQG{t5&XZ0#HY_sw5TM>iq1vPEA@;g+eCR zo7jyK*YsWqdirlLkFc@m1U|ft3oyp{BRqr#-Ffqiedvk(LWG)B+K-`QaV?QdV}A<{ zN2ltw=m6s;bWvzf`CX{IofN88s@Oj=gqQ%M=3_JTK~1gHeo&!9*m76P?CuIcS4bVs z$>wvd8T+});S8cSlj|=HB;119BWbLQ;*>_5ftyJU^^Ekjr_n?EUOkp*8SWVwDx|4W z93OREVxIWukZUM4ZE>}0JV}w`p70rXJf~8h>OmufDQJesUif!MrkKkNu57}Yvsr4B z_>Cax2ud_X%8}LvPmkgh0@1dkwqVVU8eXIqaywz@9$p%BDyJu;)!C-;RsE=#-pp{i zm@QD-U$-hfIvS_PC#``@s@E;z(C|)5E4`UME2V3JBQPHK926RlQ)MHu3=QGbP>MvW z87~AHO1NTkUwPo>jr}ip)48t+s{MbBPZQFr%9$E7T~BPSiw|0RS1Wh3|8MEi{v9K|(+E3MI{(N;>C#Ma%OrsSlgxOu#n z9Tn(pI#R>xNLaWCwOAOoyTf5+s#`C?AaJkS@N|{?&--iX&A_g{AXaf6f?Z)X6h48& zTIbKY5+;*W|}BP_vX{~m_p@fJ&MqI8dH>9-qno-sWna%<%a_UXjQQnJve0m znjzU~u(tk9C3H&k+)&FFJie;ejs>?gSLlq4STE%fg~-5m7fGkt)>cKd?J%7xq|({a zZUloHXqDzowW(^P@kn;KkRQ%C=92A!Ie2vRd;i|dgKi#m43~P-X#FiJkLW<=BCnwUzH%f} z+^wQlow}Ex5o{0gtsQ%+1=L{7DtkO?_hc1@!)hi?NnYhOSdOuWKWk$!Pv;2PgZUl| zs8YpZ3KO@?E@wE*J*%&iR@s!MZiA)a-eSIF_2r9cu9m)HhCkADf$G7dfkrW5N}36Z!y|de7$+wtVIGz%z^9Tmg$<8;)$qv|H)Xp< zcgCedIo@QCP&rjm1uINF5b=&ZJcWOg-K%L(e5+OT|92>~&hvOA-6s+1n^ zM2OQtab5C6Tb!e9Z?3Wf5F-Q(IJ`pxCk|?8;L=hopk#{v@q~7M4JzCmf_AuuCgqqJ zmNKechRpA`Q`)Zb9I^!QW&5)wTG#Zo3^7WGw_x>gFqO`@y=84H1@^4YGh%QwTCt?k zpQGgzdqC+-+c01qN@a6cgh~z5BAD`~9l^F*3;^sH-rm)26id6^HQdQoYn;@T?WMki z#zZ5-_9C)VPxh!pjdkQuM|*b-t?FPEXe5)kqPE&XS=!fj@t}~Sgw3^Au{?`gb5ClX ztUHt$N*6qpId?E*$hGdNh=5Kyr_Y5fO>wd2u2HVT>4hEds)w_*gWpC|g={Kkt!%WX za=hZhl{)9hL>cXOLc5)l4t@K`4_esZA*?+W*P|i-u-@&&{3|c^K(MN{kX6HoT6?Jt zi(!?|A&yxW=XPP)+}fHM#!SL!GL+ys>Dy~zVoFkB0Rc^nyLYutm8fmp?oRl<3wupl z)xDGi-GTwK9XWlp{H}}OFISoQWOb`cu19%r>t8jDA*j3qUS36XD>`71L*rcSRBp^} z)-ZAp7AL~0($yup4QF!60q)MFu=0VSXZ0%|j4><~0!ByU-9qa!iyKG(E};90kFtB~(p)1=Z~ zBe!plT4=+#+cd{EtnUSiTZeS_Dr~~5{kib>4eX(b_5n&6ICR5I8G9$3<^6m=7E0~g z^Xg`sv0je3zMFw+Xs7ya6HNs4{l$3Grp`>M9kYZK?yCBxyx`O+&ogaTHxgV|njvnN zTghRniCY%6b)cMgz02l!o1wnFGS{_E)ne`GO%}5`EGK4~+YyhBxqVrz9h-x5r_11m z?Cl55=3hf*_bs)tmxFZ0WU9UqjJL{-Z1fOA<=aa8JvcVz9$xn5Z_n*9 z_yOyetbOWY*>9_^!cXb?tyudxzn-WQ?qwf80sVqf3<);ZRXSz&lH$A(sBc+WkYv1a8i z#iG)Uy_}{!avjR!EOLiErofC1?IE49HxRKGZEtx;Dj0_5(nAIO-Zz5hrK--3+I4cW zy29uDIfgq`gmSE2aSn`y-|VwA{QQ zlg{>HF%P2&&(^e+8=NtcUcws8r@&^}lUnD#r$37A)o6PinKyZ1NafB!xc8r%RkbG# zJLQ?1xyTlUn{&+`s4>!EHriZ0}#tYNWl+5LiQz&1iKI12S~L?i{z}&U@%%*3aUHl24;kvu>hpYUD({K3^=3 z;E-qUEeDSyX!%DaMEhn>rCFcb?nl#ftJRxDWWV5esFpbc@5u3XJT0DmzFd|H)$a_P z93%W=Li<-wakAD`RGdz8iOz-h&O&Tg2k!!;0Y-~Vd=)mGIL*Vmrd)ns7jOIN8^L3Z zRc2S)0kjy?(u@*Tn%b5b=ZDWt=RRt;H<5eXpC z7#i>hM7i^bXNmZY2I6|7L=BEojfhrXoZcz73wNXKDMr%~ NOfReUQR$6U{~vq;-n9S# literal 0 HcmV?d00001 diff --git a/org/xeft/xeft.el b/org/xeft/xeft.el new file mode 100644 index 0000000..cf346f8 --- /dev/null +++ b/org/xeft/xeft.el @@ -0,0 +1,760 @@ +;;; xeft.el --- Deft feat. Xapian -*- lexical-binding: t; -*- + +;; Author: Yuan Fu + +;;; This file is NOT part of GNU Emacs + +;;; Commentary: +;; +;; Usage: +;; +;; Type M-x xeft RET, and you should see the Xeft buffer. Type in your +;; search phrase in the first line and the results will show up as you +;; type. Press C-n and C-p to go through each file. You can preview a +;; file by pressing SPC when the point is on a file, or click the file +;; with the mouse. Press RET to open the file in the same window. +;; +;; Type C-c C-g to force a refresh. When point is on the search +;; phrase, press RET to create a file with the search phrase as +;; the filename and title. +;; +;; Note that: +;; +;; 1. Xeft only looks for first-level files in ‘xeft-directory’. Files +;; in sub-directories are not searched unless ‘xeft-recursive’ is +;; non-nil. +;; +;; 2. Xeft creates a new file by using the search phrase as the +;; filename and title. If you want otherwise, redefine +;; ‘xeft-create-note’ or ‘xeft-filename-fn’. +;; +;; 3. Xeft saves the current window configuration before switching to +;; Xeft buffer. When Xeft buffer is killed, Xeft restores the saved +;; window configuration. +;; +;; On search queries: +;; +;; Since Xeft uses Xapian, it supports the query syntax Xapian +;; supports: +;; +;; AND, NOT, OR, XOR and parenthesizes +;; +word1 -word2 which matches documents that contains WORD1 but not +;; WORD2. +;; word1 NEAR word2 which matches documents in where word1 is near word2. +;; word1 ADJ word2 which matches documents in where word1 is near word2 +;; and word1 comes before word2 +;; "word1 word2" which matches exactly “word1 word2” +;; +;; Xeft deviates from Xapian in one aspect: consecutive phrases have +;; implied “AND” between them. So "word1 word2 word3" is actually seen +;; as "word1 AND word2 AND word3". See ‘xeft--tighten-search-phrase’ +;; for how exactly is it done. +;; +;; See https://xapian.org/docs/queryparser.html for Xapian’s official +;; documentation on query syntax. + +;;; Code: + +(require 'cl-lib) +(declare-function xapian-lite-reindex-file nil + (path dbpath &optional lang force)) +(declare-function xapian-lite-query-term nil + (term dbpath offset page-size &optional lang)) + +;;; Customize + +(defgroup xeft nil + "Xeft note interface." + :group 'applications) + +(defcustom xeft-directory "~/.deft" + "Directory in where notes are stored. Must be a full path." + :type 'directory) + +(defcustom xeft-database "~/.deft/db" + "The path to the database." + :type 'directory) + +(defcustom xeft-find-file-hook nil + "Hook run when Xeft opens a file." + :type 'hook) + +(defface xeft-selection + '((t . (:inherit region :extend t))) + "Face for the current selected search result.") + +(defface xeft-inline-highlight + '((t . (:inherit underline :extend t))) + "Face for inline highlighting in Xeft buffer.") + +(defface xeft-preview-highlight + '((t . (:inherit highlight :extend t))) + "Face for highlighting in the preview buffer.") + +(defface xeft-excerpt-title + '((t . (:inherit (bold underline)))) + "Face for the excerpt title.") + +(defface xeft-excerpt-body + '((t . (:inherit default))) + "Face for the excerpt body.") + +(defcustom xeft-default-extension "txt" + "The default extension for new files created by xeft." + :type 'string) + +(defcustom xeft-filename-fn + (lambda (search-phrase) + (concat search-phrase "." xeft-default-extension)) + "A function that takes the search phrase and returns a filename." + :type 'function) + +(defcustom xeft-ignore-extension '("iimg") + "Files with extensions in this list are ignored. + +To remove the files that you want to ignore but are already +indexed in the database, simply delete the database and start +xeft again." + :type '(list string)) + +(defcustom xeft-recursive nil + "If non-nil, xeft searches for file recursively. +Xeft doesn’t follow symlinks and ignores inaccessible directories." + :type 'boolean) + +(defcustom xeft-file-list-function #'xeft--file-list + "A function that returns files that xeft should search from. +This function takes no arguments and return a list of absolute paths." + :type 'function) + +;;; Compile + +(defun xeft--compile-module () + "Compile the dynamic module. Return non-nil if success." + ;; Just following vterm.el here. + (when (not (executable-find "make")) + (user-error "Couldn’t compile xeft: cannot find make")) + (let* ((source-dir + (shell-quote-argument + (file-name-directory + (locate-library "xeft.el" t)))) + (command (format "cd %s; make PREFIX=%s" + source-dir + (read-string "PREFIX (empty by default): "))) + (buffer (get-buffer-create "*xeft compile*"))) + (if (zerop (let ((inhibit-read-only t)) + (call-process "sh" nil buffer t "-c" command))) + (progn (message "Successfully compiled the module :-D") t) + (pop-to-buffer buffer) + (compilation-mode) + (message "Failed to compile the module") + nil))) + +(defvar xeft--linux-module-url "https://github.com/casouri/xapian-lite/releases/download/v1.0/xapian-lite-amd64-linux.so" + "URL for pre-built dynamic module for Linux.") + +(defvar xeft--mac-module-url "https://github.com/casouri/xapian-lite/releases/download/v1.0/xapian-lite-amd64-mac.dylib" + "URL for pre-built dynamic module for Mac.") + +(defun xeft--download-module () + "Download pre-built module from GitHub. Return non-nil if success." + (require 'url) + (let ((module-path (expand-file-name + "xapian-lite.so" + (file-name-directory + (locate-library "xeft.el" t))))) + (cond + ((eq system-type 'gnu/linux) + (url-copy-file xeft--linux-module-url module-path) + t) + ((eq system-type 'darwin) + (url-copy-file xeft--mac-module-url module-path) + t) + (t (message "No pre-built module for this operating system. We only have them for GNU/Linux and macOS") + nil)))) + +;;; Helpers + +(defvar xeft--last-window-config nil + "Window configuration before Xeft starts.") + +(defun xeft--buffer () + "Return the xeft buffer." + (get-buffer-create "*xeft*")) + +(defun xeft--work-buffer () + "Return the work buffer for Xeft. Used for holding file contents." + (get-buffer-create " *xeft work*")) + +(defun xeft--after-save () + "Reindex the file." + (condition-case _ + (xapian-lite-reindex-file (buffer-file-name) xeft-database) + (xapian-lite-database-lock-error + (message "The Xeft database is locked (maybe there is another Xeft instance running) so we will skip indexing this file for now")) + (xapian-lite-database-corrupt-error + (message "The Xeft database is corrupted! You should delete the database and Xeft will recreate it. Make sure other programs are not messing with Xeft database")))) + +(defvar xeft-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "RET") #'xeft-create-note) + (define-key map (kbd "C-c C-g") #'xeft-refresh-full) + (define-key map (kbd "C-c C-r") #'xeft-full-reindex) + (define-key map (kbd "C-n") #'xeft-next) + (define-key map (kbd "C-p") #'xeft-previous) + map) + "Mode map for `xeft-mode'.") + +(defvar xeft--need-refresh) +(define-derived-mode xeft-mode fundamental-mode + "Xeft" "Search for notes and display summaries." + (let ((inhibit-read-only t)) + (visual-line-mode) + (setq default-directory xeft-directory + xeft--last-window-config (current-window-configuration)) + ;; Hook ‘after-change-functions’ is too primitive, binding to that + ;; will cause problems with electric-pairs. + (add-hook 'post-command-hook + (lambda (&rest _) + (when xeft--need-refresh + (let ((inhibit-modification-hooks t)) + ;; We don’t want ‘after-change-functions’ to run + ;; when we refresh the buffer, because we set + ;; ‘xeft--need-refresh’ in that hook. + (xeft-refresh)))) + 0 t) + (add-hook 'after-change-functions + (lambda (&rest _) (setq xeft--need-refresh t)) 0 t) + (add-hook 'window-size-change-functions + (lambda (&rest _) (xeft-refresh)) 0 t) + (add-hook 'kill-buffer-hook + (lambda () + (when xeft--last-window-config + (set-window-configuration xeft--last-window-config))) + 0 t) + (erase-buffer) + (insert "\n\nInsert search phrase and press RET to search.") + (goto-char (point-min)))) + + +;;; Userland + +;;;###autoload +(defun xeft () + "Start Xeft." + (interactive) + (when (not (file-name-absolute-p xeft-directory)) + (user-error "XEFT-DIRECTORY must be an absolute path")) + (when (not (file-exists-p xeft-directory)) + (mkdir xeft-directory t)) + (when (not (file-name-absolute-p xeft-database)) + (user-error "XEFT-DATABASE must be an absolute path")) + (when (not (file-exists-p xeft-database)) + (mkdir xeft-database t)) + (unless (require 'xapian-lite nil t) + ;; I can hide download option for non-Linux/mac users, but I’m + ;; lazy. + (let* ((choice (read-char (concat + "Xeft needs the dynamic module to work, " + "download pre-built module " + (propertize "[b]" 'face 'bold) + ", compile locally " + (propertize "[c]" 'face 'bold) + ", or give up " + (propertize "[q]" 'face 'bold) + "?"))) + (success (cond ((eq choice ?b) + (xeft--download-module)) + ((eq choice ?c) + (xeft--compile-module)) + (t nil)))) + (when success + (require 'xapian-lite)))) + (if (not (featurep 'xapian-lite)) + (message "Since there is no require dynamic module, we can’t start Xeft") + (setq xeft--last-window-config (current-window-configuration)) + (switch-to-buffer (xeft--buffer)) + (when (not (derived-mode-p 'xeft-mode)) + (xeft-mode)) + ;; Reindex all files. We reindex every time M-x xeft is called. + ;; Because sometimes I use other functions to move between files, + ;; edit them, and come back to Xeft buffer to search. By that time + ;; some file are changed without Xeft noticing. + (xeft-full-reindex) + ;; Also regenerate newest file cache, for the same reason as above. + (xeft--front-page-cache-refresh))) + +(defun xeft-create-note () + "Create a new note with the current search phrase as the title." + (interactive) + (let* ((search-phrase (xeft--get-search-phrase)) + (file-name (funcall xeft-filename-fn search-phrase)) + (file-path (expand-file-name file-name xeft-directory)) + (exists-p (file-exists-p file-path))) + ;; If there is no match, create the file without confirmation, + ;; otherwise prompt for confirmation. NOTE: this is not DRY, but + ;; should be ok. + (when (or (search-forward "Press RET to create a new note" nil t) + (y-or-n-p (format "Create file `%s'? " file-name))) + (find-file file-path) + (unless exists-p + (insert search-phrase "\n\n") + (save-buffer) + ;; This should cover most cases. + (xeft--front-page-cache-refresh)) + (run-hooks 'xeft-find-file-hook)))) + +(defvar-local xeft--select-overlay nil + "Overlay used for highlighting selected search result.") + +(defun xeft--highlight-file-at-point () + "Activate (highlight) the file excerpt button at point." + (when-let ((button (button-at (point)))) + ;; Create the overlay if it doesn't exist yet. + (when (null xeft--select-overlay) + (setq xeft--select-overlay (make-overlay (button-start button) + (button-end button))) + (overlay-put xeft--select-overlay 'evaporate t) + (overlay-put xeft--select-overlay 'face 'xeft-selection)) + ;; Move the overlay over the file. + (move-overlay xeft--select-overlay + (button-start button) (button-end button)))) + +(defun xeft-next () + "Move to next file excerpt." + (interactive) + (when (forward-button 1 nil nil t) + (xeft--highlight-file-at-point))) + +(defun xeft-previous () + "Move to previous file excerpt." + (interactive) + (if (backward-button 1 nil nil t) + (xeft--highlight-file-at-point) + ;; Go to the end of the search phrase. + (goto-char (point-min)) + (end-of-line))) + +(defun xeft-full-reindex () + "Do a full reindex of all files." + (interactive) + (condition-case _ + (dolist (file (funcall xeft-file-list-function)) + (xapian-lite-reindex-file file xeft-database)) + (xapian-lite-database-lock-error + (message "The Xeft database is locked (maybe there is another Xeft instance running) so we will skip indexing for now")) + (xapian-lite-database-corrupt-error + (message "The Xeft database is corrupted! You should delete the database and Xeft will recreate it. Make sure other programs are not messing with Xeft database")))) + +;;; Draw + +(defvar xeft--preview-window nil + "Xeft shows file previews in this window.") + +(defun xeft--get-search-phrase () + "Return the search phrase. Assumes current buffer is a xeft buffer." + (save-excursion + (goto-char (point-min)) + (string-trim + (buffer-substring-no-properties (point) (line-end-position))))) + +(defun xeft--find-file-at-point () + "View file at point." + (interactive) + (find-file (button-get (button-at (point)) 'path)) + (run-hooks 'xeft-find-file-hook) + (add-hook 'after-save-hook #'xeft--after-save 0 t)) + +(defun xeft--preview-file (file &optional select) + "View FILE in another window. +If SELECT is non-nil, select the buffer after displaying it." + (interactive) + (let* ((buffer (find-file-noselect file)) + (search-phrase (xeft--get-search-phrase)) + (keyword-list (split-string search-phrase))) + (if (and (window-live-p xeft--preview-window) + (not (eq xeft--preview-window (selected-window)))) + (with-selected-window xeft--preview-window + (switch-to-buffer buffer)) + (setq xeft--preview-window + (display-buffer + buffer '((display-buffer-use-some-window + display-buffer-in-direction + display-buffer-pop-up-window) + . ((inhibit-same-window . t) + (direction . right) + (window-width + . (lambda (win) + (let ((width (window-width))) + (when (< width 50) + (window-resize + win (- 50 width) t)))))))))) + (if select (select-window xeft--preview-window)) + (with-current-buffer buffer + (xeft--highlight-matched keyword-list) + (run-hooks 'xeft-find-file-hook)))) + +(define-button-type 'xeft-excerpt + 'action (lambda (button) + ;; If the file is no already highlighted, highlight it + ;; first. + (when (not (and xeft--select-overlay + (overlay-buffer xeft--select-overlay) + (<= (overlay-start xeft--select-overlay) + (button-start button) + (overlay-end xeft--select-overlay)))) + (goto-char (button-start button)) + (xeft--highlight-file-at-point)) + (xeft--preview-file (button-get button 'path))) + 'keymap (let ((map (make-sparse-keymap))) + (set-keymap-parent map button-map) + (define-key map (kbd "RET") #'xeft--find-file-at-point) + (define-key map (kbd "SPC") #'push-button) + map) + 'help-echo "Open this file" + 'follow-link t + 'face 'default + 'mouse-face 'xeft-selection) + +(defun xeft--highlight-search-phrase () + "Highlight search phrases in buffer." + (let ((keyword-list (cl-remove-if + (lambda (word) + (or (member word '("OR" "AND" "XOR" "NOT" "NEAR")) + (string-prefix-p "ADJ" word))) + (split-string (xeft--get-search-phrase)))) + (inhibit-read-only t)) + (dolist (keyword keyword-list) + (when (> (length keyword) 1) + (goto-char (point-min)) + (forward-line 2) + ;; We use overlay because overlay allows face composition. + ;; So we can have bold + underline. + (while (search-forward keyword nil t) + (let ((ov (make-overlay (match-beginning 0) + (match-end 0)))) + (overlay-put ov 'face 'xeft-inline-highlight) + (overlay-put ov 'xeft-highlight t) + (overlay-put ov 'evaporate t))))))) + +(defvar xeft--ecache nil + "Cache for finding excerpt for a file.") + +(defun xeft--ecache-buffer (file) + "Return a buffer that has the content of FILE. +Doesn’t check for modification time, and not used." + (or (alist-get (sxhash file) xeft--ecache) + (progn + (let ((buf (get-buffer-create + (format " *xeft-ecache %s*" file)))) + (with-current-buffer buf + (setq buffer-undo-list t) + (insert-file-contents file nil nil nil t)) + (push (cons (sxhash file) buf) xeft--ecache) + (when (> (length xeft--ecache) 30) + (kill-buffer (cdr (nth 30 xeft--ecache))) + (setcdr (nthcdr 29 xeft--ecache) nil)) + buf)))) + +(defun xeft--insert-file-excerpt (file search-phrase) + "Insert an excerpt for FILE at point. +This excerpt contains note title and content excerpt and is +clickable. FILE should be an absolute path. SEARCH-PHRASE is the +search phrase the user typed." + (let ((excerpt-len (floor (* 2.7 (1- (window-width))))) + (last-search-term + (car (last (split-string search-phrase)))) + title excerpt) + (with-current-buffer (xeft--work-buffer) + (setq buffer-undo-list t) + ;; The times saved by caching is not significant enough. So I + ;; choose to not cache, but kept the code just in case. See + ;; ‘xeft--ecache-buffer’. + (insert-file-contents file nil nil nil t) + (goto-char (point-min)) + (search-forward "#+TITLE: " (line-end-position) t) + (let ((bol (point))) + (end-of-line) + (setq title (buffer-substring-no-properties bol (point)))) + (when (eq title "") (setq title "no title")) + (narrow-to-region (point) (point-max)) + ;; Grab excerpt. + (setq excerpt (string-trim + (replace-regexp-in-string + "[[:space:]]+" + " " + (if (and last-search-term + (search-forward last-search-term nil t)) + (buffer-substring-no-properties + (max (- (point) (/ excerpt-len 2)) + (point-min)) + (min (+ (point) (/ excerpt-len 2)) + (point-max))) + (buffer-substring-no-properties + (point) + (min (+ (point) excerpt-len) + (point-max)))))))) + ;; Now we insert the excerpt + (let ((start (point))) + (insert (propertize title 'face 'xeft-excerpt-title) + "\n" + (propertize excerpt 'face 'xeft-excerpt-body) + "\n\n") + ;; If we use overlay (with `make-button'), the button's face + ;; will override the bold and light face we specified above. + (make-text-button start (- (point) 2) + :type 'xeft-excerpt + 'path file)))) + +;;; Refresh and search + +(defun xeft-refresh-full () + "Refresh and display _all_ results." + (interactive) + (xeft-refresh t)) + +(defun xeft--file-list () + "Default function for ‘xeft-file-list-function’. +Return a list of all files in ‘xeft-directory’, ignoring dot +files and directories and check for ‘xeft-ignore-extension’." + (cl-remove-if-not + (lambda (file) + (and (file-regular-p file) + (not (string-prefix-p + "." (file-name-base file))) + (not (member (file-name-extension file) + xeft-ignore-extension)))) + (if xeft-recursive + (directory-files-recursively + xeft-directory "" nil (lambda (dir) + (not (string-prefix-p + "." (file-name-base dir))))) + (directory-files + xeft-directory t nil t)))) + +(defvar-local xeft--need-refresh t + "If change is made to the buffer, set this to t. +Once refreshed the buffer, set this to nil.") + +(defun xeft--tighten-search-phrase (phrase) + "Basically insert AND between each term in PHRASE." + (let ((lst (split-string phrase)) + (in-quote nil)) + ;; Basically we only insert AND between two normal phrases, and + ;; don’t insert if any of the two is an operator (AND, OR, +/-, + ;; etc), we also don’t insert AND in quoted phrases. + (string-join + (append (cl-loop for idx from 0 to (- (length lst) 2) + for this = (nth idx lst) + for next = (nth (1+ idx) lst) + collect this + if (and (not in-quote) (eq (aref this 0) ?\")) + do (setq in-quote t) + if (and in-quote + (eq (aref this (1- (length this))) ?\")) + do (setq in-quote nil) + if (not + (or in-quote + (member this '("AND" "NOT" "OR" "XOR" "NEAR")) + (string-prefix-p "ADJ" this) + (memq (aref this 0) '(?+ ?-)) + (member next '("AND" "NOT" "OR" "XOR" "NEAR")) + (string-prefix-p "ADJ" next) + (memq (aref next 0) '(?+ ?-)))) + collect "AND") + (last lst)) + " "))) + +;; This makes the integrative search results much more stable and +;; experience more fluid. And because we are not showing radically +;; different results from one key-press to another, the latency goes +;; down, I’m guessing because caching in CPU or RAM or OS or whatever. +(defun xeft--ignore-short-phrase (phrase) + "If the last term in PHRASE is too short, remove it." + (let* ((lst (or (split-string phrase) '(""))) + (last (car (last lst)))) + (if (and (not (string-match-p (rx (or (category chinese) + (category japanese) + (category korean))) + last)) + (< (length last) 3)) + (string-join (cl-subseq lst 0 (1- (length lst))) " ") + (string-join lst " ")))) + +;; See comment in ‘xeft-refresh’. +(defvar xeft--front-page-cache nil + "Stores the newest 15 or so files.") + +(defun xeft--front-page-cache-refresh () + "Refresh ‘xeft--front-page-cache’ and return it." + (setq xeft--front-page-cache + (cl-sort (funcall xeft-file-list-function) + #'file-newer-than-file-p))) + +(defun xeft-refresh (&optional full) + "Search for notes and display their summaries. +By default, only display the first 15 results. If FULL is +non-nil, display all results." + (interactive) + (when (derived-mode-p 'xeft-mode) + (let ((search-phrase (xeft--ignore-short-phrase + (xeft--get-search-phrase)))) + (let* ((phrase-empty (equal search-phrase "")) + (file-list nil) + (list-clipped nil)) + ;; 1. Get a list of files to show. + (setq file-list + ;; If the search phrase is empty (or too short and thus + ;; ignored), we show the newest files. + (if phrase-empty + (or xeft--front-page-cache + ;; Why cache? Turns out sorting this list by + ;; modification date is slow enough to be + ;; perceivable. + (setq xeft--front-page-cache + (xeft--front-page-cache-refresh))) + (xapian-lite-query-term + (xeft--tighten-search-phrase search-phrase) + xeft-database + ;; 16 is just larger than 15, so we will know it when + ;; there are more results. + 0 (if full 2147483647 16)))) + (when (and (null full) (> (length file-list) 15)) + (setq file-list (cl-subseq file-list 0 15) + list-clipped t)) + ;; 2. Display these files with excerpt. We do a + ;; double-buffering: first insert in a temp buffer, then + ;; insert the whole thing into this buffer. + (let ((inhibit-read-only t) + (orig-point (point)) + (new-content + (while-no-input + (with-temp-buffer + ;; Insert excerpts. + (if file-list + (dolist (file file-list) + (xeft--insert-file-excerpt + file search-phrase)) + ;; NOTE: this string is referred in + ;; ‘xeft-create-note’. + (unless phrase-empty + (insert "Press RET to create a new note"))) + ;; Insert clipped notice. + (when list-clipped + (insert + (format + "[Only showing the first 15 results, type %s to show all of them]\n" + (key-description + (where-is-internal #'xeft-refresh-full + xeft-mode-map t))))) + (buffer-string))))) + ;; 2.2 Actually insert the content. + (when (stringp new-content) + (while-no-input + (setq buffer-undo-list t) + (goto-char (point-min)) + (forward-line 2) + (let ((start (point))) + (delete-region (point) (point-max)) + (insert new-content) + (put-text-property (- start 2) (point) 'read-only t) + (xeft--highlight-search-phrase) + (set-buffer-modified-p nil) + ;; If finished, update this variable. + (setq xeft--need-refresh nil) + (buffer-enable-undo)))) + ;; Save excursion wouldn’t work since we erased the + ;; buffer and re-inserted contents. + (goto-char orig-point) + ;; Re-apply highlight. + (xeft--highlight-file-at-point)))))) + +;;; Highlight matched phrases + +(defun xeft--highlight-matched (keyword-list) + "Highlight keywords in KEYWORD-LIST in the current buffer." + (save-excursion + ;; Add highlight overlays. + (dolist (keyword keyword-list) + (when (> (length keyword) 1) + (goto-char (point-min)) + (while (search-forward keyword nil t) + (let ((ov (make-overlay (match-beginning 0) + (match-end 0)))) + (overlay-put ov 'face 'xeft-preview-highlight) + (overlay-put ov 'xeft-highlight t))))) + ;; Add cleanup hook. + (add-hook 'window-selection-change-functions + #'xeft--cleanup-highlight + 0 t))) + +(defun xeft--cleanup-highlight (window) + "Cleanup highlights in WINDOW." + (when (eq window (selected-window)) + (let ((ov-list (overlays-in (point-min) + (point-max)))) + (dolist (ov ov-list) + (when (overlay-get ov 'xeft-highlight) + (delete-overlay ov)))) + (remove-hook 'window-selection-change-functions + #'xeft--cleanup-highlight + t))) + +;;; Inferred links + +(defun xeft--extract-buffer-words (buffer) + "Extract words in BUFFER and return in a list. +Each element looks like (BEG . WORD) where BEG is the buffer +position of WORD." + (with-current-buffer buffer + (goto-char (point-min)) + (let (beg end word-list) + (while (progn (and (re-search-forward (rx word) nil t) + (setq beg (match-beginning 0)) + (re-search-forward (rx (not word)) nil t) + (setq end (match-beginning 0)))) + (push (cons beg (buffer-substring-no-properties beg end)) + word-list)) + (nreverse word-list)))) + +(defun xeft--generate-phrase-list (word-list max-len) + "Given WORD-LIST, generate all possible phrases up to MAX-LEN long. +Eg, given WORD-LIST = (a b c), len = 3, return + + ((a) (b) (c) (a b) (b c) (a b c))" + (cl-loop for len from 1 to max-len + append (cl-loop + for idx from 0 to (- (length word-list) len) + collect (cl-subseq word-list idx (+ idx len))))) + +(defun xeft--collect-inferred-links + (buffer max-len lower-bound upper-bound) + "Collect inferred links in BUFFER. +MAX-LEN is the same as in ‘xeft--generate-phrase-list’. Only +phrases with number of results between LOWER-BOUND and +UPPER-BOUND (inclusive) are collected." + (let* ((word-list (xeft--extract-buffer-words buffer)) + (phrase-list (xeft--generate-phrase-list + word-list max-len)) + (query-list (mapcar (lambda (phrase-list) + (let ((pos (caar phrase-list)) + (words (mapcar #'cdr phrase-list))) + (cons pos (concat "\"" + (string-join + words) + "\"")))) + phrase-list)) + (link-list + ;; QUERY-CONS = (POS . QUERY-TERM) + (cl-loop for query-cons in query-list + for file-list = (xapian-lite-query-term + (cdr query-cons) xeft-database + 0 (1+ upper-bound)) + if (<= lower-bound (length file-list) upper-bound) + collect (cons (cdr query-cons) + (length file-list))))) + link-list)) + +(provide 'xeft) + +;;; xeft.el ends here