diff --git a/.clang-format b/.clang-format index 608a707..400f60a 100644 --- a/.clang-format +++ b/.clang-format @@ -1,5 +1,5 @@ BinPackArguments: true -BreakStringLiterals: false +BreakStringLiterals: true ColumnLimit: 100 IndentCaseLabels: true IndentPPDirectives: BeforeHash diff --git a/CMakeLists.txt b/CMakeLists.txt index 56ce004..e5d6731 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,7 +32,9 @@ set(JPPRON_SRCS src/jppron/jppron.c src/jppron/database.c src/jppron/ajt_audio_index_parser.c - src/utils/yyjson.c) + src/utils/yyjson.c + src/objects/dict.c + include/objects/freqentry.h) # ############################################################################## # dictpopup @@ -46,11 +48,13 @@ add_executable(dictpopup ${JPPRON_SRCS} src/settings.c src/utils/util.c src/utils/utf8.c + src/utils/str.c src/platformdep/audio.c src/platformdep/notifications.c src/platformdep/clipboard.c src/platformdep/file_operations.c - src/platformdep/windowtitle.c) + src/platformdep/windowtitle.c + src/objects/freqentry.c) target_compile_definitions(dictpopup PUBLIC NOTIFICATIONS CLIPBOARD) target_include_directories( dictpopup PRIVATE ${GTK3_INCLUDE_DIRS} ${NOTIFY_INCLUDE_DIRS} @@ -121,12 +125,8 @@ add_executable(dictpopup-cli EXCLUDE_FROM_ALL src/frontends/cli.c ) target_include_directories( dictpopup-cli PRIVATE ${GTK3_INCLUDE_DIRS} ${CURL_INCLUDE_DIRS} include/) -target_link_directories(dictpopup-cli PRIVATE ${GTK3_LIBRARY_DIRS} -target_link_directories(cli PRIVATE ${GTK3_LIBRARY_DIRS} - ${NOTIFY_LIBRARY_DIRS} ${CURL_LIBRARY_DIRS}) -target_link_libraries(dictpopup-cli PRIVATE ${GTK3_LIBRARIES} ${NOTIFY_LIBRARIES} -target_link_libraries(cli PRIVATE ${GTK3_LIBRARIES} ${NOTIFY_LIBRARIES} - ${CURL_LIBRARIES} -lmecab -llmdb) +target_link_directories(dictpopup-cli PRIVATE ${GTK3_LIBRARY_DIRS}) +target_link_libraries(dictpopup-cli PRIVATE ${GTK3_LIBRARIES} ${NOTIFY_LIBRARIES}) # ############################################################################## # Tests @@ -146,8 +146,9 @@ if (BUILD_TESTING) tests/jppron_tests/ajt_audio_index_parser_tests.c tests/utils_tests/enclose_word_in_string_tests.c tests/dictpopup_tests.c - src/utils/utf8.c) - target_compile_definitions(c_tests PRIVATE UNIT_TEST) + src/utils/utf8.c + tests/jppron_tests/jppron_tests.c) + target_compile_definitions(c_tests PRIVATE UNIT_TEST CLIPBOARD) target_include_directories(c_tests PRIVATE ${GTK3_INCLUDE_DIRS} ${CGREEN_INCLUDE_DIRS} include/ src/ src/jppron) target_link_libraries( c_tests diff --git a/Makefile b/Makefile index b564c39..ed892f8 100644 --- a/Makefile +++ b/Makefile @@ -81,7 +81,7 @@ cli: $(SRC) $(SRC_H) $(SDIR)/frontends/cli.c $(CC) $(CFLAGS) $(DEBUG_CFLAGS) $(CPPFLAGS) -o $@ $(SDIR)/frontends/cli.c $(SRCS) $(LDLIBS) deinflector: $(SRC) $(SRC_H) $(SDIR)/deinflector.c - $(CC) $(CFLAGS) $(DEBUG_CFLAGS) $(CPPFLAGS) -DDEINFLECTOR_MAIN -o $@ $(SDIR)/deinflector.c $(SDIR)/util.c $(LDLIBS) + $(CC) $(CFLAGS) $(DEBUG_CFLAGS) $(CPPFLAGS) -DDEINFLECTOR_MAIN -o $@ $(SDIR)/deinflector.c $(SDIR)/utils/util.c $(SDIR)/utils/utf8.c $(LDLIBS) release: version=$$(git describe); prefix=dictpopup-$${version#v}; \ diff --git a/TODO b/TODO index 2748455..adbf258 100644 --- a/TODO +++ b/TODO @@ -1,6 +1,8 @@ ## dictpopup - Write a "Yomichan search"-like window application - Check if sentence is mapped to some field and only prompt for it in that case +- Cache dot indicator result for kanji/reading pairs and lookup for every dict entry +- Disable pronunciation button if nothing found Mid priority - Implement the reading label as a text view to allow for editing -> initiate new search on Enter press diff --git a/include/ankiconnectc.h b/include/ankiconnectc.h index c8411d4..716368b 100644 --- a/include/ankiconnectc.h +++ b/include/ankiconnectc.h @@ -3,6 +3,7 @@ #include "utils/util.h" #include +#include typedef struct { char *deck; @@ -21,7 +22,7 @@ s8 *ac_get_decks(char **error); s8 *ac_get_notetypes(char **error); int ac_check_exists(char *deck, char *field, char *entry, char **error); void ac_gui_search(const char *deck, const char *field, const char *entry, char **error); -void ac_addNote(ankicard ac, char **error); +void _nonnull_ ac_addNote(ankicard ac, char **error); /* * Stores the file at @path in the Anki media collection under the name diff --git a/include/db.h b/include/db.h index 0c6e871..d74b7a7 100644 --- a/include/db.h +++ b/include/db.h @@ -1,6 +1,7 @@ #ifndef DP_DB_H #define DP_DB_H - +#include "objects/dict.h" +#include "objects/freqentry.h" #include "utils/util.h" #include @@ -11,14 +12,15 @@ __attribute__((returns_nonnull)) database_t *_nonnull_ db_open(char *dbpath, boo void _nonnull_ db_close(database_t *db); void _nonnull_ db_put_dictent(database_t *db, dictentry de); -void _nonnull_ db_get_dictents(const database_t *db, s8 headword, dictentry *dict[static 1]); +void _nonnull_ db_append_lookup(const database_t *db, s8 headword, dictentry *dict[static 1], + bool is_deinflection); void _nonnull_ db_put_freq(const database_t *db, freqentry fe); /* * Checks if there exists a database in the provided path */ -i32 db_check_exists(s8 dbpath); +bool db_check_exists(s8 dbpath); void db_remove(s8 dbpath); DEFINE_DROP_FUNC(database_t *, db_close) diff --git a/include/deinflector.h b/include/deinflector.h index 2db81b2..c72959f 100644 --- a/include/deinflector.h +++ b/include/deinflector.h @@ -1,5 +1,7 @@ #include "utils/util.h" +#include + /* * @word: The japanese word to be deinflected * diff --git a/include/dictpopup.h b/include/dictpopup.h index c2f9550..c148068 100644 --- a/include/dictpopup.h +++ b/include/dictpopup.h @@ -1,10 +1,11 @@ #ifndef DP_DICTPOPUP_H #define DP_DICTPOPUP_H -#include "utils/util.h" - #include +#include "utils/util.h" +#include "objects/dict.h" +typedef dictentry* Dict; /* * Should only be called once and as early as possible */ @@ -14,24 +15,8 @@ void dictpopup_init(void); * Looks up @lookup in the database and returns all corresponding dictentries in * a buffer (see include/buf.h) */ -dictentry *_nonnull_ create_dictionary(s8 *word); +Dict _nonnull_ create_dictionary(s8 *word, Config config); void create_ankicard(s8 lookup, dictentry de, Config config); -#define POSSIBLE_ENTRIES_S_NMEMB 9 -typedef struct possible_entries_s { - s8 copiedsent; - s8 boldsent; - s8 dictkanji; - s8 dictreading; - s8 dictdefinition; - s8 furigana; - s8 windowname; - s8 dictname; -} possible_entries_s; - -struct dictpopup_s { - possible_entries_s pe; -}; - #endif diff --git a/include/jppron/ajt_audio_index_parser.h b/include/jppron/ajt_audio_index_parser.h index 6ac39f0..1aaa078 100644 --- a/include/jppron/ajt_audio_index_parser.h +++ b/include/jppron/ajt_audio_index_parser.h @@ -1,8 +1,7 @@ #ifndef AUDIO_INDEX_PARSER_H #define AUDIO_INDEX_PARSER_H -#include -#include "utils/util.h" +#include "utils/str.h" typedef struct { s8 origin; diff --git a/include/jppron/database.h b/include/jppron/database.h index 6c6bf89..4f05594 100644 --- a/include/jppron/database.h +++ b/include/jppron/database.h @@ -4,6 +4,7 @@ #include "ajt_audio_index_parser.h" #include "utils/util.h" #include +#include typedef struct database_s database; diff --git a/include/jppron/jppron.h b/include/jppron/jppron.h index 0dfaa98..cc34773 100644 --- a/include/jppron/jppron.h +++ b/include/jppron/jppron.h @@ -9,10 +9,10 @@ typedef struct { fileinfo_s fileinfo; } pronfile_s; -void _nonnull_ free_pronfile(pronfile_s *pronfile); -void _nonnull_ free_pronfile_buffer(pronfile_s **pronfiles); +void free_pronfile(pronfile_s pronfile[static 1]); +void free_pronfile_buffer(pronfile_s *pronfiles); DEFINE_DROP_FUNC_PTR(pronfile_s, free_pronfile) -DEFINE_DROP_FUNC_PTR(pronfile_s *, free_pronfile_buffer) +DEFINE_DROP_FUNC(pronfile_s *, free_pronfile_buffer) void jppron(s8 word, s8 reading, char *audio_folders_path); /* _deallocator_(free_pronfile_buffer) */ // TODO: Check why this gives compiler errors diff --git a/include/objects/dict.h b/include/objects/dict.h new file mode 100644 index 0000000..54b731b --- /dev/null +++ b/include/objects/dict.h @@ -0,0 +1,32 @@ +#ifndef DICT_H +#define DICT_H +#include "utils/str.h" +#include "utils/util.h" + +typedef struct { + s8 dictname; + s8 kanji; + s8 reading; + s8 definition; + u32 frequency; + bool is_deinflection; +} dictentry; + +dictentry dictentry_dup(dictentry de); +void _nonnull_ dictentry_free(dictentry de); +void dictentry_print(dictentry de); + +typedef dictentry *Dict; +Dict newDict(void); +bool isEmpty(Dict dict); +void _nonnull_ dictionary_add(Dict *dict, dictentry de); +isize dictLen(Dict dict); + +// Sorts @dict in place +void dictSort(Dict dict, int (*dictentryComparer)(const dictentry *a, const dictentry *b)); + +void _nonnull_ dictionary_free(Dict *dict); +dictentry dictentry_at_index(Dict dict, isize index); +dictentry *pointer_to_entry_at(Dict dict, isize index); + +#endif // DICT_H diff --git a/include/objects/freqentry.h b/include/objects/freqentry.h new file mode 100644 index 0000000..5aed6d0 --- /dev/null +++ b/include/objects/freqentry.h @@ -0,0 +1,14 @@ +#ifndef FREQENTRY_H +#define FREQENTRY_H +#include + +typedef struct { + s8 word; + s8 reading; + u32 frequency; +} freqentry; + +freqentry freqentry_dup(freqentry fe); +void freqentry_free(freqentry *fe); + +#endif // FREQENTRY_H diff --git a/include/platformdep/audio.h b/include/platformdep/audio.h index 6e3fe66..0f55ddd 100644 --- a/include/platformdep/audio.h +++ b/include/platformdep/audio.h @@ -1,8 +1,8 @@ #ifndef AUDIO_H #define AUDIO_H -#include "utils/util.h" +#include void play_audio(s8 filepath); -#endif //AUDIO_H +#endif // AUDIO_H diff --git a/include/platformdep/clipboard.h b/include/platformdep/clipboard.h index 5627308..4845319 100644 --- a/include/platformdep/clipboard.h +++ b/include/platformdep/clipboard.h @@ -1,10 +1,10 @@ #ifndef CLIPBOARD_H #define CLIPBOARD_H -#include "utils/util.h" +#include s8 get_selection(void); -s8 get_clipboard(void); -s8 get_next_clipboard(void); +char *get_clipboard(void); +char *get_next_clipboard(void); -#endif //CLIPBOARD_H +#endif // CLIPBOARD_H diff --git a/include/platformdep/file_operations.h b/include/platformdep/file_operations.h index 381ef82..ff60d31 100644 --- a/include/platformdep/file_operations.h +++ b/include/platformdep/file_operations.h @@ -1,8 +1,8 @@ #ifndef FILE_OPERATIONS_H #define FILE_OPERATIONS_H -#include #include "utils/util.h" +#include void _nonnull_ createdir(char *dirpath); const char *get_user_data_dir(void); @@ -11,4 +11,4 @@ void rem(char *filepath); void _nonnull_ file_copy_sync(const char *source, const char *dest); void _nonnull_ file_copy_async(const char *source, const char *dest); -#endif //FILE_OPERATIONS_H +#endif // FILE_OPERATIONS_H diff --git a/include/platformdep/file_paths.h b/include/platformdep/file_paths.h index 4d1bb79..267a5bb 100644 --- a/include/platformdep/file_paths.h +++ b/include/platformdep/file_paths.h @@ -7,11 +7,23 @@ #ifdef _WIN32 // TODO: fill - #define DEFAULT_DATABASE_LOCATIONS (char*[]){""} - #define DEFAULT_SETTINGS_LOCATIONS (char*[]){"", ""} + #define DEFAULT_DATABASE_LOCATIONS \ + (char *[]) { \ + "" \ + } + #define DEFAULT_SETTINGS_LOCATIONS \ + (char *[]) { \ + "", "" \ + } #else - #define DEFAULT_DATABASE_LOCATIONS (char*[]){"/usr/share/dictpopup/data.mdb", "/usr/local/share/dictpopup/data.mdb"} - #define DEFAULT_SETTINGS_LOCATIONS (char*[]){"/usr/share/dictpopup/config.ini", "/usr/local/share/dictpopup/config.ini"} + #define DEFAULT_DATABASE_LOCATIONS \ + (char *[]) { \ + "/usr/share/dictpopup/data.mdb", "/usr/local/share/dictpopup/data.mdb" \ + } + #define DEFAULT_SETTINGS_LOCATIONS \ + (char *[]) { \ + "/usr/share/dictpopup/config.ini", "/usr/local/share/dictpopup/config.ini" \ + } #endif -#endif //FILE_PATHS_H +#endif // FILE_PATHS_H diff --git a/include/platformdep/notifications.h b/include/platformdep/notifications.h index 0d5641d..629cb6a 100644 --- a/include/platformdep/notifications.h +++ b/include/platformdep/notifications.h @@ -3,4 +3,4 @@ void notify(_Bool urgent, char const *fmt, ...); -#endif //NOTIFICATIONS_H +#endif // NOTIFICATIONS_H diff --git a/include/platformdep/windowtitle.h b/include/platformdep/windowtitle.h index 473e30c..a34da62 100644 --- a/include/platformdep/windowtitle.h +++ b/include/platformdep/windowtitle.h @@ -1,7 +1,8 @@ #ifndef WINDOWTITLE_H #define WINDOWTITLE_H -#include "utils/util.h" +#include + s8 get_windowname(void); #endif //WINDOWTITLE_H \ No newline at end of file diff --git a/include/settings.h b/include/settings.h index abeead8..0ef1668 100644 --- a/include/settings.h +++ b/include/settings.h @@ -5,8 +5,8 @@ typedef struct { struct { - char *dbpth; - bool sort; + char *dbDir; + bool sortDictEntries; char **dictSortOrder; bool nukeWhitespaceLookup; bool mecab; diff --git a/include/messages.h b/include/utils/messages.h similarity index 100% rename from include/messages.h rename to include/utils/messages.h diff --git a/include/utils/str.h b/include/utils/str.h new file mode 100644 index 0000000..1fc649d --- /dev/null +++ b/include/utils/str.h @@ -0,0 +1,109 @@ +#ifndef S8_H +#define S8_H + +#include +#include "utils/util.h" + +typedef struct { + u8 *s; + isize len; +} s8; + +// This encodes an invalid UTF-8 char to be used as a stopper for variable argument macros +#define S8_STOPPER \ + (s8) { \ + .s = (u8[]){0xF8}, .len = 1 \ + } +#define lengthof(s) (arrlen("" s "") - 1) +#define s8(s) \ + { (u8 *)s, arrlen(s) - 1 } +#define S(s) (s8) s8(s) + +/* + * Allocates a new s8 with length @len + * The containing string is null-terminated + */ +s8 news8(isize len); + +i32 u8compare(u8 *a, u8 *b, isize n); +/* + * Copies @src into @dst returning the remaining portion of @dst + */ +s8 s8copy(s8 dst, s8 src); + +/* + * Returns a copy of s + */ +s8 s8dup(s8 src); +/* + * Turns @z into an s8 string, reusing the pointer. + */ +s8 fromcstr_(char *z); +/* + * Returns a true value if a is equal to b + */ +i32 s8equals(s8 a, s8 b); + +s8 cuthead(s8 s, isize off); +s8 takehead(s8 s, isize len); +s8 cuttail(s8 s, isize len); +s8 taketail(s8 s, isize len); +bool startswith(s8 s, s8 prefix); +bool endswith(s8 s, s8 suffix); + +/* + * Returns s with the last UTF-8 character removed + * Expects s to have length > 0 + */ +s8 s8striputf8chr(s8 s); + +/* + * Turns escaped characters such as the string "\\n" into the character '\n' + * (inplace) + * + * Returns: unescaped string + */ +s8 unescape(s8 str); + +void strip_trailing_whitespace(s8 *str); + +void _nonnull_ frees8(s8 *z); +DEFINE_DROP_FUNC_PTR(s8, frees8) + +void frees8buffer(s8 *buf); +DEFINE_DROP_FUNC(s8 *, frees8buffer) + +/* + * Concatenates all s8 strings passed as argument + * + * Returns: A newly allocated s8 containing the concatenated string + */ +#define concat(...) concat_((s8[]){__VA_ARGS__, S8_STOPPER}) +s8 _nonnull_ concat_(s8 *strings); + +#define buildpath(...) buildpath_((s8[]){__VA_ARGS__, S8_STOPPER}) +s8 _nonnull_ buildpath_(s8 *pathcomps); +s8 enclose_word_in_s8_with(s8 str, s8 word, s8 prefix, s8 suffix); + + +/* --------------------------- string builder -----------------------_ */ +typedef struct { + u8 *data; + isize len; + isize cap; +} stringbuilder_s; + +stringbuilder_s sb_init(size_t init_cap); +void sb_append(stringbuilder_s *b, s8 str); +void sb_append_char(stringbuilder_s *sb, char c); +char *sb_steal_str(stringbuilder_s *sb); +s8 sb_gets8(stringbuilder_s sb); +s8 sb_steals8(stringbuilder_s sb); +void _nonnull_ sb_set(stringbuilder_s *sb, s8 s); +void _nonnull_ sb_free(stringbuilder_s *sb); +/* ------------------------------------------------------------------ */ + +void substrremove(char *str, const s8 sub); +void nuke_whitespace(s8 *z); + +#endif //S8_H diff --git a/include/utils/utf8.h b/include/utils/utf8.h index 898d894..2d3eb72 100644 --- a/include/utils/utf8.h +++ b/include/utils/utf8.h @@ -11,15 +11,16 @@ * Returns the length in bytes of the utf8 encoded char pointed to by @p */ #define utf8_chr_len(p) utf8_chr_len_data[(p)[0] >> 4] +#include "str.h" +#include "util.h" extern u8 const utf8_chr_len_data[]; -s8 convert_to_utf8(char *str); +s8 convertToUtf8(char *str); /* * Strips last utf8 character * Warning: Not NULL-terminated */ -s8 striputf8chr(s8 s); +void _nonnull_ stripUtf8Char(s8 s[static 1]); - -#endif //UTF8_H +#endif // UTF8_H diff --git a/include/utils/util.h b/include/utils/util.h index e69f070..ed138c9 100644 --- a/include/utils/util.h +++ b/include/utils/util.h @@ -6,7 +6,6 @@ #include // close() typedef unsigned char u8; -typedef signed int b32; typedef signed int i32; typedef unsigned int u32; typedef __PTRDIFF_TYPE__ isize; @@ -47,139 +46,8 @@ __attribute__((malloc, returns_nonnull)) _deallocator_(free) void *xrealloc(void #define new(type, num) xcalloc(num, sizeof(type)) // clang-format on -/* ------------------- Start s8 utils ---------------- */ -// This encodes an invalid UTF-8 char to be used as a stopper -// for variable argument length macros -#define S8_STOPPER \ - (s8) { \ - .s = (u8[]){0xF8}, .len = 1 \ - } -#define lengthof(s) (arrlen("" s "") - 1) -#define s8(s) \ - { (u8 *)s, arrlen(s) - 1 } -#define S(s) (s8) s8(s) - -typedef struct { - u8 *s; - isize len; -} s8; - -/* - * Allocates a new s8 with length @len - * The containing string is null-terminated - */ -s8 news8(isize len); - -i32 u8compare(u8 *a, u8 *b, isize n); -/* - * Copies @src into @dst returning the remaining portion of @dst - */ -s8 s8copy(s8 dst, s8 src); - -/* - * Returns a copy of s - */ -s8 s8dup(s8 src); -/* - * Turns @z into an s8 string, reusing the pointer. - */ -s8 fromcstr_(char *z); -/* - * Returns a true value if a is equal to b - */ -i32 s8equals(s8 a, s8 b); - -s8 cuthead(s8 s, isize off); -s8 takehead(s8 s, isize len); -s8 cuttail(s8 s, isize len); -s8 taketail(s8 s, isize len); -b32 startswith(s8 s, s8 prefix); -b32 endswith(s8 s, s8 suffix); - -/* - * Returns s with the last UTF-8 character removed - * Expects s to have length > 0 - */ -s8 s8striputf8chr(s8 s); - -/* - * Turns escaped characters such as the string "\\n" into the character '\n' - * (inplace) - * - * Returns: unescaped string - */ -s8 unescape(s8 str); - -void strip_trailing_whitespace(s8 *str); - -void _nonnull_ frees8(s8 *z); -void frees8buffer(s8 *buf); - -/* - * Concatenates all s8 strings passed as argument - * - * Returns: A newly allocated s8 containing the concatenated string - */ -#define concat(...) concat_((s8[]){__VA_ARGS__, S8_STOPPER}) -s8 _nonnull_ concat_(s8 *strings); - -#define buildpath(...) buildpath_((s8[]){__VA_ARGS__, S8_STOPPER}) -s8 _nonnull_ buildpath_(s8 *pathcomps); -s8 enclose_word_in_s8_with(s8 str, s8 word, s8 prefix, s8 suffix); -/* ------------------- End s8 utils ---------------- */ - -/* --------------------------- string builder -----------------------_ */ -typedef struct { - u8 *data; - isize len; - isize cap; -} stringbuilder_s; - -stringbuilder_s sb_init(size_t init_cap); -void sb_append(stringbuilder_s *b, s8 str); -void sb_append_char(stringbuilder_s *sb, char c); -char *sb_steal_str(stringbuilder_s *sb); -s8 sb_gets8(stringbuilder_s sb); -s8 sb_steals8(stringbuilder_s sb); -void _nonnull_ sb_set(stringbuilder_s *sb, s8 s); -void _nonnull_ sb_free(stringbuilder_s *sb); -/* ------------------------------------------------------------------_ */ - -/* --------------------- Start dictentry / dictionary ----------------- */ -typedef struct { - s8 dictname; - s8 kanji; - s8 reading; - s8 definition; - u32 frequency; -} dictentry; - -dictentry dictentry_dup(dictentry de); -void _nonnull_ dictentry_free(dictentry de); -void dictentry_print(dictentry de); -void _nonnull_ dictionary_add(dictentry **dict, dictentry de); -isize dictlen(dictentry *dict); -void _nonnull_ dictionary_free(dictentry **dict); -dictentry dictentry_at_index(dictentry *dict, isize index); -dictentry *pointer_to_entry_at(dictentry *dict, isize index); -/* --------------------- End dictentry ------------------------ */ - -/* --------------------- Start freqentry ----------------- */ -typedef struct { - s8 word; - s8 reading; - u32 frequency; -} freqentry; - -freqentry freqentry_dup(freqentry fe); -void freqentry_free(freqentry *fe); -/* --------------------- End freqentry ------------------------ */ - size_t _printf_(3, 4) snprintf_safe(char *buf, size_t len, const char *fmt, ...); -void substrremove(char *str, const s8 sub); - - /** * __attribute__((cleanup)) functions */ @@ -206,7 +74,5 @@ static inline void drop_close(int *fd) { } DEFINE_DROP_FUNC_VOID(free) -DEFINE_DROP_FUNC_PTR(s8, frees8) -DEFINE_DROP_FUNC(s8 *, frees8buffer) #endif diff --git a/src/ankiconnectc/ankiconnectc.c b/src/ankiconnectc/ankiconnectc.c index 57832e0..2833222 100644 --- a/src/ankiconnectc/ankiconnectc.c +++ b/src/ankiconnectc/ankiconnectc.c @@ -4,7 +4,7 @@ #include #include "ankiconnectc.h" -#include "messages.h" +#include "utils/messages.h" #ifndef UNIT_TEST #include "send_request.c" @@ -37,25 +37,31 @@ static char *json_escape_str(const char *str) { case '\f': sb_append(&sb, S("\\f")); break; - case '\n': - sb_append(&sb, S("
")); - break; case '\r': sb_append(&sb, S("\\r")); break; - case '\t': - sb_append(&sb, S(" ")); // html tab - break; case '"': sb_append(&sb, S("\\\"")); break; case '\\': sb_append(&sb, S("\\\\")); break; + // Anki escape + case '\t': + sb_append(&sb, S(" ")); // html tab + break; + case '\n': + sb_append(&sb, S("
")); + break; + case '<': + sb_append(&sb, S("<")); + break; + case '>': + sb_append(&sb, S(">")); + break; default: sb_append_char(&sb, *str); } - str++; } @@ -145,7 +151,7 @@ static s8 form_search_req(bool include_suspended, bool include_new, char *deck, include_new ? S("") : S(" -is:new"), S("\" } }")); } -static int ac_check_exists_with(bool include_suspended, bool include_new, char *deck, char *field, +static int check_exists_with_(bool include_suspended, bool include_new, char *deck, char *field, char *str, char **error) { _drop_(frees8) s8 req = form_search_req(include_suspended, include_new, deck, field, str); retval_s r = sendRequest(req, search_checker); @@ -173,19 +179,19 @@ int ac_check_exists(char *deck, char *field, char *lookup, char **error) { return -1; } - int rc = ac_check_exists_with(false, false, deck, field, lookup, error); + int rc = check_exists_with_(false, false, deck, field, lookup, error); if (rc == -1) return -1; if (rc == 1) return 1; - rc = ac_check_exists_with(false, true, deck, field, lookup, error); + rc = check_exists_with_(false, true, deck, field, lookup, error); if (rc == -1) return -1; if (rc == 1) return 2; - rc = ac_check_exists_with(true, true, deck, field, lookup, error); + rc = check_exists_with_(true, true, deck, field, lookup, error); if (rc == -1) return -1; if (rc == 1) diff --git a/src/ankiconnectc/send_request.c b/src/ankiconnectc/send_request.c index 6db44fb..60d323a 100644 --- a/src/ankiconnectc/send_request.c +++ b/src/ankiconnectc/send_request.c @@ -3,6 +3,14 @@ */ #include #include "utils/util.h" +#include +#include +#include "ankiconnectc/send_request.h" + +#define AC_API_URL_EVAR "ANKICONNECT_API_URL" +#define DEFAULT_AC_API_URL "http://localhost:8765" + +typedef size_t (*ResponseFunc)(char *ptr, size_t len, size_t nmemb, void *userdata); static size_t noop_write_function(char *ptr, size_t size, size_t nmemb, void *userdata) { (void)ptr; // Suppress unused parameter warning diff --git a/src/db.c b/src/db.c index 709fc37..80c11fa 100644 --- a/src/db.c +++ b/src/db.c @@ -2,8 +2,8 @@ #include #include "db.h" -#include "messages.h" #include "platformdep/file_operations.h" +#include "utils/messages.h" DEFINE_DROP_FUNC(MDB_cursor *, mdb_cursor_close) @@ -219,22 +219,24 @@ static s8 getdata(const database_t *db, u32 id) { return (s8){.s = data.mv_data, .len = data.mv_size}; } -void db_get_dictents(const database_t *db, s8 headword, dictentry *dict[static 1]) { +// TODO: Refactor this +void db_append_lookup(const database_t *db, s8 headword, dictentry *dict[static 1], + bool is_deinflection) { dbg("Looking up: %.*s", (int)headword.len, (char *)headword.s); size_t n_ids = 0; _drop_(free) u32 *ids = getids(db, headword, &n_ids); - // u32 *ids = getids(db, headword, &n_ids); if (ids) { for (size_t i = 0; i < n_ids; i++) { s8 de_data = getdata(db, ids[i]); dictentry de = data_to_dictent(db, de_data); + de.is_deinflection = is_deinflection; dictionary_add(dict, de); } } } -i32 db_check_exists(s8 dbpath) { +bool db_check_exists(s8 dbpath) { _drop_(frees8) s8 dbfile = buildpath(dbpath, S("data.mdb")); return check_file_exists((char *)dbfile.s); } diff --git a/src/deinflector.c b/src/deinflector.c index 33fa228..e1a29df 100644 --- a/src/deinflector.c +++ b/src/deinflector.c @@ -7,9 +7,11 @@ #include #include "deinflector.h" -#include "messages.h" -#include "utils/util.h" +#include "utils/messages.h" #include "utils/utf8.h" +#include "utils/util.h" + +#include typedef enum { UNKNOWN, VERB, ADJ_I } wtype; @@ -494,7 +496,7 @@ s8 *deinflect(s8 word) { #ifdef DEINFLECTOR_MAIN int main(int argc, char **argv) { die_on(argc < 2, "Usage: "); - s8 *d = deinflect(fromcstr_(argv[1])); + _drop_(frees8buffer) s8 *d = deinflect(fromcstr_(argv[1])); for (size_t i = 0; i < buf_size(d); i++) { printf("%.*s\n", (int)d[i].len, (char *)d[i].s); } diff --git a/src/dictpopup.c b/src/dictpopup.c index c877c0e..9b1bd90 100644 --- a/src/dictpopup.c +++ b/src/dictpopup.c @@ -9,14 +9,15 @@ #include "db.h" #include "deinflector.h" #include "dictpopup.h" -#include "messages.h" #include "settings.h" +#include "utils/messages.h" #include "utils/utf8.h" #include "utils/util.h" #include "platformdep/file_operations.h" -#include "platformdep/windowtitle.h" #include "platformdep/file_paths.h" +#include "platformdep/windowtitle.h" +#include "utils/str.h" #ifdef CLIPBOARD #include "platformdep/clipboard.h" #endif @@ -27,68 +28,62 @@ s8 focused_window_title = {0}; -// TODO: Cleaner (and non-null terminated) implementation -static void nuke_whitespace(s8 *z) { - substrremove((char *)z->s, S("\n")); - substrremove((char *)z->s, S("\t")); - substrremove((char *)z->s, S(" ")); - substrremove((char *)z->s, S(" ")); +#define POSSIBLE_ENTRIES_S_NMEMB 9 +typedef struct possible_entries_s { + s8 copiedsent; + s8 boldsent; + s8 dictkanji; + s8 dictreading; + s8 dictdefinition; + s8 furigana; + s8 windowname; + s8 dictname; +} possible_entries_s; + +static void _nonnull_ appendDeinflections(const database_t *db, s8 word, + dictentry *dict[static 1]) { + _drop_(frees8buffer) s8 *deinfs_b = deinflect(word); - *z = fromcstr_((char *)z->s); + for (size_t i = 0; i < buf_size(deinfs_b); i++) + db_append_lookup(db, deinfs_b[i], dict, true); } -static s8 add_bold_tags_around_word(s8 sent, s8 word) { - return enclose_word_in_s8_with(sent, word, S(""), S("")); +static Dict _nonnull_ lookup_word(s8 word, database_t *db) { + Dict dict = newDict(); + db_append_lookup(db, word, &dict, false); + appendDeinflections(db, word, &dict); + return dict; } -static s8 create_furigana(s8 kanji, s8 reading) { - return (!kanji.len && !reading.len) ? S("") - /* : !reading.len ? S("") // Don't try */ - : s8equals(kanji, reading) ? s8dup(reading) - : concat(kanji, S("["), reading, - S("]")); // TODO: Obviously not - // enough if kanji - // contains hiragana -} +static Dict _nonnull_ lookup_first_matching_prefix(s8 *word, database_t *db) { + dictentry *dict = newDict(); -static s8 get_sentence(void) { -#ifdef CLIPBOARD - if (cfg.anki.copySentence) { - msg("Please select the context."); - s8 clip = get_next_clipboard(); - if (cfg.anki.nukeWhitespaceSentence) - nuke_whitespace(&clip); - return clip; - } else -#endif - { - return (s8){0}; + int firstChrLen = utf8_chr_len(word->s); + while (isEmpty(dict) && word->len > firstChrLen) { + stripUtf8Char(word); + if (word->len < MAX_LOOKUP_LEN) { // Don't waste time looking up huge strings + dict = lookup_word(*word, db); + } } -} - -static void fill_entries(possible_entries_s pe[static 1], s8 lookup, dictentry const de) { - pe->copiedsent = get_sentence(); - pe->boldsent = add_bold_tags_around_word(pe->copiedsent, lookup); - pe->dictdefinition = de.definition; - pe->dictkanji = de.kanji.len > 0 ? de.kanji : de.reading; - pe->dictreading = de.reading; - pe->furigana = create_furigana(de.kanji, de.reading); - pe->dictname = de.dictname; + return dict; } -static void _nonnull_ add_deinflections_to_dict(const database_t *db, s8 word, - dictentry *dict[static 1]) { - _drop_(frees8buffer) s8 *deinfs_b = deinflect(word); - - for (size_t i = 0; i < buf_size(deinfs_b); i++) - db_get_dictents(db, deinfs_b[i], dict); +static Dict _nonnull_ lookup_hiragana_conversion(s8 word, database_t *db) { + _drop_(frees8) s8 hira = kanji2hira(word); + return lookup_word(hira, db); } -static dictentry _nonnull_ *lookup(const database_t *db, s8 word) { - dictentry *dict = NULL; - db_get_dictents(db, word, &dict); - add_deinflections_to_dict(db, word, &dict); +static Dict _nonnull_ lookup(s8 *word, Config config) { + _drop_(db_close) database_t *db = db_open(config.general.dbDir, true); + + Dict dict = lookup_word(*word, db); + if (isEmpty(dict) && config.general.mecab) { + dict = lookup_hiragana_conversion(*word, db); + } + if (isEmpty(dict) && config.general.substringSearch) { + dict = lookup_first_matching_prefix(word, db); + } return dict; } @@ -102,91 +97,129 @@ static int indexof(char const *str, char *arr[]) { return INT_MAX; } -static int _nonnull_ dictentry_comparer(void const *voida, void const *voidb) { - dictentry a = *(dictentry *)voida; - dictentry b = *(dictentry *)voidb; - +/* + * -1 means @a comes first + */ +static int _nonnull_ dictentry_comparer(const dictentry *a, const dictentry *b) { int inda, indb; - if (s8equals(a.dictname, b.dictname)) { - inda = a.frequency == 0 ? INT_MAX : a.frequency; - indb = b.frequency == 0 ? INT_MAX : b.frequency; + + // TODO: Make this less cryptic + if (a->is_deinflection ^ b->is_deinflection) { + inda = a->is_deinflection; + indb = b->is_deinflection; + } else if (s8equals(a->dictname, b->dictname)) { + inda = a->frequency == 0 ? INT_MAX : a->frequency; + indb = b->frequency == 0 ? INT_MAX : b->frequency; } else { - inda = indexof((char *)a.dictname.s, cfg.general.dictSortOrder); - indb = indexof((char *)b.dictname.s, cfg.general.dictSortOrder); + inda = indexof((char *)a->dictname.s, cfg.general.dictSortOrder); + indb = indexof((char *)b->dictname.s, cfg.general.dictSortOrder); } return inda < indb ? -1 : inda == indb ? 0 : 1; } -static void sort_dictentries(dictentry *dict) { - if (dict) - qsort(dict, dictlen(dict), sizeof(dictentry), dictentry_comparer); -} +Dict _nonnull_ create_dictionary(s8 *word, Config config) { + if (config.general.nukeWhitespaceLookup) + nuke_whitespace(word); -static dictentry *lookup_first_matching_prefix(s8 *word, database_t *db) { - dictentry *dict = NULL; + Dict dict = lookup(word, config); - int first_chr_len = utf8_chr_len(word->s); - while (dict == NULL && word->len > first_chr_len) { - *word = striputf8chr(*word); - word->s[word->len] = '\0'; // TODO: Remove necessity for this - if (word->len < MAX_LOOKUP_LEN) { // Don't waste time looking up huge strings - dict = lookup(db, *word); - } - } + if (config.general.sortDictEntries) + dictSort(dict, dictentry_comparer); return dict; } -dictentry *create_dictionary(s8 *word) { - assert(word->len); - dictentry *dict = NULL; - - if (cfg.general.nukeWhitespaceLookup) - nuke_whitespace(word); - - _drop_(db_close) database_t *db = db_open(cfg.general.dbpth, true); - - dict = lookup(db, *word); +/* ---------------- Anki related ----------------- */ +static s8 add_bold_tags_around_word(s8 sent, s8 word) { + return enclose_word_in_s8_with(sent, word, S(""), S("")); +} - if (dict == NULL && cfg.general.mecab) { - _drop_(frees8) s8 hira = kanji2hira(*word); - dict = lookup(db, hira); - } +static s8 create_furigana(s8 kanji, s8 reading) { + return (!kanji.len && !reading.len) ? S("") + : !reading.len ? S("") // Don't even try + : s8equals(kanji, reading) ? s8dup(reading) + : concat(kanji, S("["), reading, + S("]")); // TODO: Obviously not + // enough if kanji + // contains hiragana +} - if (dict == NULL && cfg.general.substringSearch) { - dict = lookup_first_matching_prefix(word, db); +static s8 get_sentence(void) { +#ifdef CLIPBOARD + if (cfg.anki.copySentence) { + msg("Please select the context."); + s8 clip = fromcstr_(get_next_clipboard()); + if (cfg.anki.nukeWhitespaceSentence) + nuke_whitespace(&clip); + return clip; + } else +#endif + { + return (s8){0}; } +} - if (cfg.general.sort) - sort_dictentries(dict); +static void _nonnull_ fill_entries(possible_entries_s *pe, s8 lookup, dictentry const de) { + pe->copiedsent = get_sentence(); + pe->boldsent = add_bold_tags_around_word(pe->copiedsent, lookup); - return dict; + pe->dictdefinition = de.definition; + pe->dictkanji = de.kanji.len > 0 ? de.kanji : de.reading; + pe->dictreading = de.reading; + pe->furigana = create_furigana(de.kanji, de.reading); + pe->dictname = de.dictname; } -static s8 map_entry(possible_entries_s p, int i) { +static s8 map_entry(s8 lookup, dictentry de, int i) { // A safer way would be switching to strings, but I feel like that's // not very practical to configure - return i == 0 ? S("") - : i == 2 ? p.copiedsent - : i == 3 ? p.boldsent - : i == 4 ? p.dictkanji - : i == 5 ? p.dictreading - : i == 6 ? p.dictdefinition - : i == 7 ? p.furigana - : i == 8 ? p.windowname - : i == 9 ? p.dictname - : S(""); + // return i == 0 ? S("") + // : i == 2 ? p.copiedsent + // : i == 3 ? p.boldsent + // : i == 4 ? p.dictkanji + // : i == 5 ? p.dictreading + // : i == 6 ? p.dictdefinition + // : i == 7 ? p.furigana + // : i == 8 ? p.windowname + // : i == 9 ? p.dictname + // : S(""); + + switch (i) { + case 0: + return S(""); + case 1: + return lookup; + case 2: + return get_sentence(); + case 3: + return add_bold_tags_around_word(get_sentence(), lookup); + case 4: + return de.kanji; + case 5: + return de.reading; + case 6: + return de.definition; + case 7: + return create_furigana(de.kanji, de.reading); + case 8: + return focused_window_title; + case 9: + return de.dictname; + default: + err("Anki field mapping number %i is out of bounds.", i); + return S(""); + } } static ankicard prepare_ankicard(s8 lookup, dictentry de, Config config) { - possible_entries_s p = {.windowname = focused_window_title}; - fill_entries(&p, lookup, de); + // possible_entries_s p = {.windowname = focused_window_title}; + // fill_entries(&p, lookup, de); char **fieldentries = new (char *, config.anki.numFields); for (size_t i = 0; i < config.anki.numFields; i++) { - fieldentries[i] = (char *)map_entry(p, config.anki.fieldMapping[i]).s; + fieldentries[i] = (char *)map_entry(lookup, de, config.anki.fieldMapping[i]).s; } return (ankicard){.deck = config.anki.deck, @@ -211,19 +244,22 @@ void create_ankicard(s8 lookup, dictentry de, Config config) { send_ankicard(ac); free(ac.fieldentries); } +/* ----------------- End Anki related ------------------- */ -static void copy_default_database(char *dbdir) { +static void copy_default_database_to(char *path) { const char *default_db_loc = NULL; - for(size_t i = 0; i < sizeof(DEFAULT_DATABASE_LOCATIONS); i++) { - if(check_file_exists(DEFAULT_DATABASE_LOCATIONS[i])) + for (size_t i = 0; i < arrlen(DEFAULT_DATABASE_LOCATIONS); i++) { + if (check_file_exists(DEFAULT_DATABASE_LOCATIONS[i])) { default_db_loc = DEFAULT_DATABASE_LOCATIONS[i]; + break; + } } - die_on(!default_db_loc, + die_on(default_db_loc == NULL, "Could not access the default database either. You need to create your own with" "dictpopup-create or download data.mdb from the repository."); - createdir(dbdir); - _drop_(frees8) s8 dbpath = buildpath(fromcstr_(dbdir), S("data.mdb")); + createdir(path); + _drop_(frees8) s8 dbpath = buildpath(fromcstr_(path), S("data.mdb")); file_copy_sync(default_db_loc, (char *)dbpath.s); } @@ -231,10 +267,10 @@ void dictpopup_init(void) { setlocale(LC_ALL, ""); read_user_settings(POSSIBLE_ENTRIES_S_NMEMB); - if (!db_check_exists(fromcstr_(cfg.general.dbpth))) { + if (!db_check_exists(fromcstr_(cfg.general.dbDir))) { msg("No database found. We recommend creating your own database with dictpopup-create, but " "copying default dictionary for now.."); - copy_default_database(cfg.general.dbpth); + copy_default_database_to(cfg.general.dbDir); } focused_window_title = get_windowname(); diff --git a/src/dictpopup_create.c b/src/dictpopup_create.c index db6de76..d731fc2 100644 --- a/src/dictpopup_create.c +++ b/src/dictpopup_create.c @@ -8,7 +8,7 @@ #include #include "db.h" -#include "messages.h" +#include "utils/messages.h" #include "utils/util.h" #include "yomichan_parser.h" diff --git a/src/frontends/gtk3popup.c b/src/frontends/gtk3popup.c index 755cca5..e541d46 100644 --- a/src/frontends/gtk3popup.c +++ b/src/frontends/gtk3popup.c @@ -7,9 +7,9 @@ #include "jppron/jppron.h" #include "settings.h" -#include "messages.h" #include "platformdep/clipboard.h" +#include "utils/messages.h" #include "utils/utf8.h" #include "utils/util.h" @@ -165,7 +165,7 @@ static void update_win_title(dictentry current_entry) { static void update_dictnum_label(int cur_entry_index) { char tmp[20] = {0}; - snprintf_safe(tmp, arrlen(tmp), "%i/%li", cur_entry_index + 1, dictlen(dict)); + snprintf_safe(tmp, arrlen(tmp), "%i/%li", cur_entry_index + 1, dictLen(dict)); gtk_label_set_text(GTK_LABEL(dw.lbl_dictnum), tmp); } @@ -213,12 +213,12 @@ static void update_widgets(int cur_entry_num) { } static void _nonnull_ previous_dictentry(int *cur_dictentry_num) { - *cur_dictentry_num = (*cur_dictentry_num != 0) ? *cur_dictentry_num - 1 : dictlen(dict) - 1; + *cur_dictentry_num = (*cur_dictentry_num != 0) ? *cur_dictentry_num - 1 : dictLen(dict) - 1; update_widgets(*cur_dictentry_num); } static void _nonnull_ next_dictentry(int *cur_entry_num) { - *cur_entry_num = (*cur_entry_num < dictlen(dict) - 1) ? *cur_entry_num + 1 : 0; + *cur_entry_num = (*cur_entry_num < dictLen(dict) - 1) ? *cur_entry_num + 1 : 0; update_widgets(*cur_entry_num); } @@ -271,7 +271,7 @@ static void add_to_anki_from_clipboard(GtkWidget *widget, gpointer user_data) { prepare_add_to_anki(lv->winargs, *lv->cur_entry_num); - s8 clip = get_clipboard(); + s8 clip = fromcstr_(get_clipboard()); set_anki_definition(lv->winargs, clip); close_window(lv); @@ -345,10 +345,8 @@ static void button_press_on_dot_indicator(GtkWidget *self, GdkEventButton *event gint *cur_dictentry_num = user_data; if (event->type == GDK_BUTTON_PRESS) { g_mutex_lock(&dict_mutex); - _drop_(frees8) s8 word_dup = dictentry_at(*cur_dictentry_num).kanji; + search_in_anki_browser(dictentry_at(*cur_dictentry_num).kanji); g_mutex_unlock(&dict_mutex); - - search_in_anki_browser(word_dup); } } @@ -563,7 +561,7 @@ static gpointer dictionary_lookup_thread(gpointer voidin) { s8 *lookup = voidin; static bool first_invocation = true; - dictentry *local_dict = create_dictionary(lookup); + dictentry *local_dict = create_dictionary(lookup, cfg); if (local_dict == NULL) { msg("No dictionary entry found"); @@ -589,8 +587,8 @@ static int parse_cmd_line_opts(int argc, char **argv) { break; case 'd': if (optarg && *optarg) { - free(cfg.general.dbpth); // 注意 - cfg.general.dbpth = strdup(optarg); + free(cfg.general.dbDir); // 注意 + cfg.general.dbDir = strdup(optarg); } break; case 'h': @@ -621,7 +619,7 @@ int main(int argc, char *argv[]) { int nextarg = parse_cmd_line_opts(argc, argv); // Should be second to overwrite settings _drop_(frees8) s8 lookup = - argc - nextarg > 0 ? convert_to_utf8(argv[nextarg]) : get_selection(); + argc - nextarg > 0 ? convertToUtf8(argv[nextarg]) : get_selection(); die_on(!lookup.len, "No selection and no argument provided. Exiting.."); die_on(!g_utf8_validate((char *)lookup.s, lookup.len, NULL), "Lookup is not a valid UTF-8 string."); @@ -633,4 +631,6 @@ int main(int argc, char *argv[]) { if (winargs.create_ac) create_ankicard(lookup, winargs.de, cfg); + + dictentry_free(winargs.de); } \ No newline at end of file diff --git a/src/jppron/ajt_audio_index_parser.c b/src/jppron/ajt_audio_index_parser.c index bebfea6..d4a5abc 100644 --- a/src/jppron/ajt_audio_index_parser.c +++ b/src/jppron/ajt_audio_index_parser.c @@ -1,10 +1,13 @@ #include "jppron/ajt_audio_index_parser.h" -#include -#include -#include +#include "utils/str.h" + +#include "utils/messages.h" +#include "utils/str.h" #include "utils/util.h" #include "utils/yyjson.h" +#include +#include typedef struct { s8 root_path; @@ -42,8 +45,8 @@ static void parse_meta(yyjson_val *metaobj, index_meta *im) { } } -static void parse_headwords(yyjson_val *headwordobj, index_meta im, void (*foreach_headword)(void *, s8, s8), - void *userdata_hw) { +static void parse_headwords(yyjson_val *headwordobj, index_meta im, + void (*foreach_headword)(void *, s8, s8), void *userdata_hw) { err_ret_on(!yyjson_is_obj(headwordobj), "Headword entry in index.json does not consist of an object."); @@ -64,7 +67,7 @@ static void parse_headwords(yyjson_val *headwordobj, index_meta im, void (*forea } } -static fileinfo_s extract_fileinfo(yyjson_val * fileinfoobj) { +static fileinfo_s extract_fileinfo(yyjson_val *fileinfoobj) { fileinfo_s fi = {0}; size_t objidx, objmax; @@ -73,11 +76,10 @@ static fileinfo_s extract_fileinfo(yyjson_val * fileinfoobj) { s8 key = yyjson_get_s8(objkey); s8 val = yyjson_get_s8(objval); - if(s8equals(key, S("kana_reading"))) { + if (s8equals(key, S("kana_reading"))) { kata2hira(val); // Warning: changes json. I think yyjson expects immutable fi.hira_reading = val; - } - else if (s8equals(key, S("pitch_number"))) + } else if (s8equals(key, S("pitch_number"))) fi.pitch_number = val; else if (s8equals(key, S("pitch_pattern"))) fi.pitch_pattern = val; @@ -86,13 +88,15 @@ static fileinfo_s extract_fileinfo(yyjson_val * fileinfoobj) { return fi; } -static void parse_files(yyjson_val * filesobj, index_meta im, void(*foreach_file)(void *, s8, fileinfo_s), void * userdata_f){ +static void parse_files(yyjson_val *filesobj, index_meta im, + void (*foreach_file)(void *, s8, fileinfo_s), void *userdata_f) { err_ret_on(!yyjson_is_obj(filesobj), "Value of \"files\" is not an object."); size_t objidx, objmax; yyjson_val *objkey, *objval; yyjson_obj_foreach(filesobj, objidx, objmax, objkey, objval) { - err_ret_on(!yyjson_is_obj(objval), "Value of key: '%s' is not an object.", yyjson_get_str(objkey)); + err_ret_on(!yyjson_is_obj(objval), "Value of key: '%s' is not an object.", + yyjson_get_str(objkey)); s8 fn = yyjson_get_s8(objkey); fileinfo_s fi = extract_fileinfo(objval); @@ -117,7 +121,7 @@ void parse_audio_index_from_file(s8 curdir, const char *index_filepath, err_ret_on(!yyjson_is_obj(rootobj), "Invalid audio index format: Dictionary not consisting of an object."); - index_meta im = {.root_path = curdir }; + index_meta im = {.root_path = curdir}; bool seen_meta = false; size_t idx, max; @@ -128,13 +132,16 @@ void parse_audio_index_from_file(s8 curdir, const char *index_filepath, if (s8equals(s8key, S("meta"))) { parse_meta(objval, &im); seen_meta = true; - } - else if (s8equals(s8key, S("headwords"))) { - err_ret_on(!seen_meta, "Index in '%s' orders \"headwords\" before \"meta\". This is not supported yet.", index_filepath); + } else if (s8equals(s8key, S("headwords"))) { + err_ret_on( + !seen_meta, + "Index in '%s' orders \"headwords\" before \"meta\". This is not supported yet.", + index_filepath); parse_headwords(objval, im, foreach_headword, userdata_hw); - } - else if (s8equals(s8key, S("files"))) { - err_ret_on(!seen_meta, "Index in '%s' orders \"files\" before \"meta\". This is not supported yet.", index_filepath); + } else if (s8equals(s8key, S("files"))) { + err_ret_on(!seen_meta, + "Index in '%s' orders \"files\" before \"meta\". This is not supported yet.", + index_filepath); parse_files(objval, im, foreach_file, userdata_f); } } diff --git a/src/jppron/database.c b/src/jppron/database.c index 71cd750..c3a29c0 100644 --- a/src/jppron/database.c +++ b/src/jppron/database.c @@ -9,9 +9,10 @@ #include "jppron/database.h" -#include "messages.h" -#include "utils/util.h" #include "jppron/ajt_audio_index_parser.h" +#include "utils/messages.h" +#include "utils/str.h" +#include "utils/util.h" DEFINE_DROP_FUNC(MDB_cursor *, mdb_cursor_close) @@ -123,9 +124,9 @@ static fileinfo_s convert_data_to_fileinfo(s8 data) { } return (fileinfo_s){.origin = data_split[0], - .hira_reading = data_split[1], - .pitch_number = data_split[2], - .pitch_pattern = data_split[3]}; + .hira_reading = data_split[1], + .pitch_number = data_split[2], + .pitch_pattern = data_split[3]}; } void jdb_add_file_with_fileinfo(database *db, s8 filepath, fileinfo_s fi) { diff --git a/src/jppron/jppron.c b/src/jppron/jppron.c index 215d391..a8d525f 100644 --- a/src/jppron/jppron.c +++ b/src/jppron/jppron.c @@ -10,9 +10,9 @@ #include "jppron/jppron.h" #include "deinflector.h" -#include "messages.h" #include "platformdep/audio.h" #include "platformdep/file_operations.h" +#include "utils/messages.h" #include "utils/util.h" DEFINE_DROP_FUNC(DIR *, closedir) @@ -81,36 +81,55 @@ static s8 normalize_reading(s8 reading) { return r; } -void free_pronfile(pronfile_s *pronfile) { +void free_pronfile(pronfile_s pronfile[static 1]) { frees8(&pronfile->filepath); freefileinfo(&pronfile->fileinfo); } -void free_pronfile_buffer(pronfile_s **pronfiles) { - while (buf_size(*pronfiles) > 0) - free_pronfile(&buf_pop(*pronfiles)); - buf_free(*pronfiles); +void free_pronfile_buffer(pronfile_s *pronfiles) { + while (buf_size(pronfiles) > 0) + free_pronfile(&buf_pop(pronfiles)); + buf_free(pronfiles); } -pronfile_s *get_pronfiles_for(s8 word, s8 reading, s8 db_path) { +static fileinfo_s *get_fileinfo_for_array(database *db, s8 *files) { + fileinfo_s *fi = new(fileinfo_s, buf_size(files)); + for (size_t i = 0; i < buf_size(files); i++) { + fi[i] = jdb_get_fileinfo(db, files[i]); + } + return fi; +} + +static void get_all_files_and_fileinfo_for(s8 word, s8 db_path, s8 *files[static 1], fileinfo_s *fileinfo[static 1]) { _drop_(jdb_close) database *db = jdb_open((char *)db_path.s, true); - _drop_(frees8) s8 normread = normalize_reading(reading); - _drop_(frees8buffer) s8 *files = jdb_get_files(db, word); - pronfile_s *pronfiles = NULL; - if (!files) + *files = jdb_get_files(db, word); + if (!*files) + return; + + *fileinfo = get_fileinfo_for_array(db, *files); +} + +pronfile_s *get_pronfiles_for(s8 word, s8 reading, s8 db_path) { + _drop_(frees8buffer) s8 *files = 0; + _drop_(free) fileinfo_s *fileinfo = 0; + get_all_files_and_fileinfo_for(word, db_path, &files, &fileinfo); + + if(!files) return NULL; + _drop_(frees8) s8 normread = normalize_reading(reading); + + pronfile_s *pronfiles = NULL; + if (reading.len) { for (size_t i = 0; i < buf_size(files); i++) { - fileinfo_s fi = jdb_get_fileinfo(db, files[i]); - - if (s8equals(normread, fi.hira_reading)) { - pronfile_s pf = (pronfile_s){.filepath = files[i], .fileinfo = fi}; - files[i] = (s8){0}; // Transfer ownership + if (s8equals(normread, fileinfo[i].hira_reading)) { + pronfile_s pf = (pronfile_s){.filepath = files[i], .fileinfo = fileinfo[i]}; + // Transfer ownership + files[i] = (s8){0}; + fileinfo[i] = (fileinfo_s){0}; buf_push(pronfiles, pf); - } else { - freefileinfo(&fi); } } } @@ -118,13 +137,17 @@ pronfile_s *get_pronfiles_for(s8 word, s8 reading, s8 db_path) { if (!pronfiles) { // Add all for (size_t i = 0; i < buf_size(files); i++) { - fileinfo_s fi = jdb_get_fileinfo(db, files[i]); - pronfile_s pf = (pronfile_s){.filepath = files[i], .fileinfo = fi}; + pronfile_s pf = (pronfile_s){.filepath = files[i], .fileinfo = fileinfo[i]}; files[i] = (s8){0}; + fileinfo[i] = (fileinfo_s){0}; buf_push(pronfiles, pf); } } + for (size_t i = 0; i < buf_size(files); i++) { + freefileinfo(&fileinfo[i]); + } + return pronfiles; } @@ -158,7 +181,7 @@ void jppron(s8 word, s8 reading, char *audio_folders_path) { pronfile_s *pronfiles = get_pronfiles_for(word, reading, dbpath); if (pronfiles) { play_pronfiles(pronfiles); - free_pronfile_buffer(&pronfiles); + free_pronfile_buffer(pronfiles); } else msg("No pronunciation found."); diff --git a/src/objects/dict.c b/src/objects/dict.c new file mode 100644 index 0000000..54963bb --- /dev/null +++ b/src/objects/dict.c @@ -0,0 +1,63 @@ +#include "objects/dict.h" +#include "utils/str.h" + +dictentry dictentry_dup(dictentry de) { + return (dictentry){.dictname = s8dup(de.dictname), + .kanji = s8dup(de.kanji), + .reading = s8dup(de.reading), + .definition = s8dup(de.definition)}; +} + +void dictentry_print(dictentry de) { + printf("dictname: %s\n" + "kanji: %s\n" + "reading: %s\n" + "definition: %s\n", + (char *)de.dictname.s, (char *)de.kanji.s, (char *)de.reading.s, + (char *)de.definition.s); +} + +Dict newDict() { + return NULL; +} + +bool isEmpty(Dict dict) { + return buf_size(dict) == 0; +} + +void dictionary_add(dictentry **dict, dictentry de) { + buf_push(*dict, de); +} + +isize dictLen(dictentry *dict) { + return buf_size(dict); +} + +void dictSort(Dict dict, int (*dictentryComparer)(const dictentry *a, const dictentry *b)) { + if (!isEmpty(dict)) + qsort(dict, dictLen(dict), sizeof(dictentry), + (int (*)(const void *, const void *))dictentryComparer); +} + +void dictentry_free(dictentry de) { + frees8(&de.dictname); + frees8(&de.kanji); + frees8(&de.reading); + frees8(&de.definition); +} + +void dictionary_free(dictentry **dict) { + while (buf_size(*dict) > 0) + dictentry_free(buf_pop(*dict)); + buf_free(*dict); +} + +dictentry dictentry_at_index(dictentry *dict, isize index) { + assert(index >= 0 && (size_t)index < buf_size(dict)); + return dict[index]; +} + +dictentry *pointer_to_entry_at(dictentry *dict, isize index) { + assert(index >= 0 && (size_t)index < buf_size(dict)); + return dict + index; +} diff --git a/src/objects/freqentry.c b/src/objects/freqentry.c new file mode 100644 index 0000000..394033c --- /dev/null +++ b/src/objects/freqentry.c @@ -0,0 +1,12 @@ +#include "objects/freqentry.h" +#include + +freqentry freqentry_dup(freqentry fe) { + return (freqentry){ + .word = s8dup(fe.word), .reading = s8dup(fe.reading), .frequency = fe.frequency}; +} + +void freqentry_free(freqentry *fe) { + frees8(&fe->word); + frees8(&fe->reading); +} diff --git a/src/platformdep/audio.c b/src/platformdep/audio.c index 929eb1a..e9eb208 100644 --- a/src/platformdep/audio.c +++ b/src/platformdep/audio.c @@ -1,8 +1,8 @@ #include #include "platformdep/audio.h" -#include "utils/util.h" -#include "messages.h" +#include "utils/messages.h" +#include void play_audio(s8 filepath) { _drop_(frees8) s8 cmd = diff --git a/src/platformdep/clipboard.c b/src/platformdep/clipboard.c index 5f76ca3..07c99dc 100644 --- a/src/platformdep/clipboard.c +++ b/src/platformdep/clipboard.c @@ -3,6 +3,8 @@ #include "platformdep/clipboard.h" #include "utils/util.h" +#include + // Instead of gtk one could also use: https://github.com/jtanx/libclipboard s8 get_selection(void) { gtk_init(NULL, NULL); @@ -10,10 +12,10 @@ s8 get_selection(void) { return fromcstr_(gtk_clipboard_wait_for_text(clipboard)); } -s8 get_clipboard(void) { +char *get_clipboard(void) { gtk_init(NULL, NULL); GtkClipboard *clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); - return fromcstr_(gtk_clipboard_wait_for_text(clipboard)); + return gtk_clipboard_wait_for_text(clipboard); } static void cb_changed_callback(GtkClipboard *clipboard, gpointer user_data) { @@ -31,7 +33,7 @@ static void wait_cb_change(void) { * Wait until clipboard changes and return new clipboard contents. * Can return NULL. */ -s8 get_next_clipboard(void) { +char *get_next_clipboard(void) { wait_cb_change(); return get_clipboard(); } \ No newline at end of file diff --git a/src/platformdep/file_operations.c b/src/platformdep/file_operations.c index 6e714e7..ee37ac5 100644 --- a/src/platformdep/file_operations.c +++ b/src/platformdep/file_operations.c @@ -1,9 +1,9 @@ - #include +#include #include #include "platformdep/file_operations.h" -#include "messages.h" +#include "utils/messages.h" void createdir(char *dirpath) { int ret = g_mkdir_with_parents(dirpath, 0777); @@ -22,7 +22,7 @@ void rem(char *filepath) { remove(filepath); } -void file_copy_sync(const char* source_path, const char* dest_path) { +void file_copy_sync(const char *source_path, const char *dest_path) { g_autoptr(GFile) source = g_file_new_for_path(source_path); g_autoptr(GFile) dest = g_file_new_for_path(dest_path); @@ -40,7 +40,7 @@ static void copy_ready_cb(GObject *source_object, GAsyncResult *res, gpointer da dbg("Error copying file: %s", error->message); } -void file_copy_async(const char* source_path, const char* dest_path) { +void file_copy_async(const char *source_path, const char *dest_path) { g_autoptr(GFile) source = g_file_new_for_path(source_path); g_autoptr(GFile) dest = g_file_new_for_path(dest_path); g_file_copy_async(source, dest, G_FILE_COPY_NONE, G_PRIORITY_LOW, NULL, NULL, NULL, diff --git a/src/platformdep/windowtitle.c b/src/platformdep/windowtitle.c index 2994e1d..3a70222 100644 --- a/src/platformdep/windowtitle.c +++ b/src/platformdep/windowtitle.c @@ -3,12 +3,13 @@ #include #endif -#include "utils/util.h" -#include "messages.h" #include "platformdep/windowtitle.h" +#include "utils/messages.h" + +#include s8 get_windowname(void) { - #ifdef HAVEX11 +#ifdef HAVEX11 Display *dpy = XOpenDisplay(NULL); if (!dpy) { dbg("Can't open X display for retrieving the window title. Are you " @@ -33,7 +34,7 @@ s8 get_windowname(void) { &actual_type, &format, &nr_items, &bytes_after, &prop) == Success && prop) { break; - } + } } XCloseDisplay(dpy); @@ -41,8 +42,8 @@ s8 get_windowname(void) { s8 ret = s8dup(fromcstr_((char *)prop)); // s.t. cann be freed with free XFree(prop); return ret; - #else +#else // Not implemented return (s8){0}; - #endif +#endif } \ No newline at end of file diff --git a/src/settings.c b/src/settings.c index 15fa224..943a84a 100644 --- a/src/settings.c +++ b/src/settings.c @@ -2,26 +2,25 @@ #include // dirname() #include #include -#include -#include // getopt #include #include -#include "messages.h" #include "platformdep/file_operations.h" #include "settings.h" +#include "utils/messages.h" #include "utils/util.h" #include +#include Config cfg = {0}; static Config get_default_cfg(void) { Config default_cfg = { - .general.sort = 0, + .general.sortDictEntries = 0, .general.dictSortOrder = NULL, - .general.dbpth = NULL, // Set by set_runtime_defaults() + .general.dbDir = NULL, // Set by set_runtime_defaults() .general.nukeWhitespaceLookup = 1, .general.mecab = 0, .general.substringSearch = 1, @@ -50,8 +49,8 @@ static Config get_default_cfg(void) { } static void set_runtime_defaults(void) { - if (!cfg.general.dbpth) { - cfg.general.dbpth = + if (!cfg.general.dbDir) { + cfg.general.dbDir = (char *)buildpath(fromcstr_((char *)g_get_user_data_dir()), S("dictpopup")).s; } } @@ -76,8 +75,8 @@ void print_settings(void) { // TODO: Finish implementing puts("Settings:"); puts("[General]"); - printf("Database path: '%s'\n", cfg.general.dbpth); - printf("Sort: %s\n", printyn(cfg.general.sort)); + printf("Database path: '%s'\n", cfg.general.dbDir); + printf("Sort: %s\n", printyn(cfg.general.sortDictEntries)); printf("Dictionary sort order: "); print_array(cfg.general.dictSortOrder); printf("Remove whitespace from lookup: %s\n", printyn(cfg.general.nukeWhitespaceLookup)); @@ -179,7 +178,8 @@ static void read_uint_list(GKeyFile *kf, const char *group, const char *key, u32 for (gsize i = 0; i < *length; i++) { if (val[i] < 0) { - err("Received a negative value at index '%li' in key: '%s' and group '%s' when expecting a positive.", + err("Received a negative value at index '%li' in key: '%s' and group '%s' when " + "expecting a positive.", i, key, group); return; } @@ -189,9 +189,9 @@ static void read_uint_list(GKeyFile *kf, const char *group, const char *key, u32 } static void read_general(GKeyFile *kf) { - read_string(kf, "General", "DatabasePath", &cfg.general.dbpth); - read_bool(kf, "General", "Sort", &cfg.general.sort); - if (cfg.general.sort) + read_string(kf, "General", "DatabasePath", &cfg.general.dbDir); + read_bool(kf, "General", "Sort", &cfg.general.sortDictEntries); + if (cfg.general.sortDictEntries) read_string_list(kf, "General", "DictSortOrder", &cfg.general.dictSortOrder, NULL); read_bool(kf, "General", "NukeWhitespaceLookup", &cfg.general.nukeWhitespaceLookup); read_bool(kf, "General", "MecabConversion", &cfg.general.mecab); @@ -239,8 +239,8 @@ static void read_pronunciation(GKeyFile *kf) { static void copy_default_config_to(char *filepath) { const char *default_config_loc = NULL; - for(size_t i = 0; i < sizeof(DEFAULT_SETTINGS_LOCATIONS); i++) { - if(check_file_exists(DEFAULT_SETTINGS_LOCATIONS[i])) + for (size_t i = 0; i < arrlen(DEFAULT_SETTINGS_LOCATIONS); i++) { + if (check_file_exists(DEFAULT_SETTINGS_LOCATIONS[i])) default_config_loc = DEFAULT_SETTINGS_LOCATIONS[i]; } if (!default_config_loc) { @@ -266,12 +266,13 @@ static s8 get_config_filepath(void) { S("config.ini")); } -void read_config_from_keyfile(GKeyFile_autoptr kf) { +static void read_config_from_keyfile(GKeyFile_autoptr kf) { read_general(kf); read_anki(kf); read_popup(kf); read_pronunciation(kf); } + void read_user_settings(int fieldmapping_max) { cfg = get_default_cfg(); // TODO: Put this to the end and only set missing values @@ -282,10 +283,11 @@ void read_user_settings(int fieldmapping_max) { g_autoptr(GError) error = NULL; if (!g_key_file_load_from_file(kf, (char *)cfgfile.s, G_KEY_FILE_NONE, &error)) { if (g_error_matches(error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) { - err("Could not find a config file in: \"%s\". Copying default config.. ", cfgfile.s); + err("Could not find a config file in: \"%s\". Copying default config.. ", + (char *)cfgfile.s); copy_default_config_to((char *)cfgfile.s); // Uses the default config from above though } else - err("Error opening \"%s\": %s. Falling back to default config.", cfgfile.s, + err("Error opening \"%s\": %s. Falling back to default config.", (char *)cfgfile.s, error->message); } else { read_config_from_keyfile(kf); diff --git a/src/utils/str.c b/src/utils/str.c new file mode 100644 index 0000000..353a0a6 --- /dev/null +++ b/src/utils/str.c @@ -0,0 +1,306 @@ + +#include "utils/str.h" + +#include + +/* --------------- Start s8 utils -------------- */ +i32 u8compare(u8 *restrict a, u8 *restrict b, isize n) { + for (; n; n--) { + i32 d = *a++ - *b++; + if (d) + return d; + } + return 0; +} + +/* + * Copy src into dst returning the remaining portion of dst. + */ +s8 s8copy(s8 dst, s8 src) { + assert(dst.len >= src.len && src.len >= 0); + + if (src.s && dst.s) { // Passing null to memcpy is undefined behaviour ... + memcpy(dst.s, src.s, src.len); + dst.s += src.len; + dst.len -= src.len; + } + return dst; +} + +s8 news8(isize len) { + assert(len >= 0); + return (s8){.s = new (u8, len + 1), // Include NULL terminator + .len = len}; +} + +s8 s8dup(s8 src) { +#ifdef UNIT_TEST + if (!src.s) + return (s8){0}; +#endif + + s8 r = news8(src.len); + s8copy(r, src); + return r; +} + +s8 fromcstr_(char *z) { + s8 s = {0}; + s.s = (u8 *)z; + s.len = z ? strlen(z) : 0; + return s; +} + +i32 s8equals(s8 a, s8 b) { + return a.len == b.len && !u8compare(a.s, b.s, a.len); +} + +s8 cuthead(s8 s, isize off) { + assert(off >= 0); + assert(off <= s.len); + s.s += off; + s.len -= off; + return s; +} + +s8 takehead(s8 s, isize len) { + assert(len >= 0); + assert(len <= s.len); + s.len = len; + return s; +} + +s8 cuttail(s8 s, isize len) { + assert(len >= 0); + assert(len <= s.len); + s.len -= len; + return s; +} + +s8 taketail(s8 s, isize len) { + return cuthead(s, s.len - len); +} + +bool startswith(s8 s, s8 prefix) { + return s.len >= prefix.len && s8equals(takehead(s, prefix.len), prefix); +} + +bool endswith(s8 s, s8 suffix) { + return s.len >= suffix.len && s8equals(taketail(s, suffix.len), suffix); +} + +s8 unescape(s8 str) { + isize s = 0, e = 0; + while (e < str.len) { + if (str.s[e] == '\\' && e + 1 < str.len) { + switch (str.s[e + 1]) { + case 'n': + str.s[s++] = '\n'; + e += 2; + break; + case '\\': + str.s[s++] = '\\'; + e += 2; + break; + case '"': + str.s[s++] = '"'; + e += 2; + break; + case 'r': + str.s[s++] = '\r'; + e += 2; + break; + default: + str.s[s++] = str.s[e++]; + } + } else + str.s[s++] = str.s[e++]; + } + str.len = s; + + return str; +} + +s8 concat_(s8 *strings) { + isize len = 0; + for (s8 *s = strings; !s8equals(*s, S8_STOPPER); s++) + len += s->len; + + s8 ret = news8(len); + s8 p = ret; + for (s8 *s = strings; !s8equals(*s, S8_STOPPER); s++) + p = s8copy(p, *s); + return ret; +} + +// TODO: Don't append sep when already there +s8 buildpath_(s8 *pathcomps) { +#ifdef _WIN32 + s8 sep = S("\\"); +#else + s8 sep = S("/"); +#endif + isize pathlen = 0; + bool first = true; + for (s8 *pc = pathcomps; !s8equals(*pc, S8_STOPPER); pc++) { + if (!first) + pathlen += sep.len; + pathlen += pc->len; + + first = false; + } + + s8 retpath = news8(pathlen); + s8 p = retpath; + + first = true; + for (s8 *pc = pathcomps; !s8equals(*pc, S8_STOPPER); pc++) { + if (!first) + p = s8copy(p, sep); + p = s8copy(p, *pc); + + first = false; + } + + return retpath; +} + +static bool whitespace(u8 c) { + switch (c) { + case '\t': + case '\n': + case '\b': + case '\f': + case '\r': + case ' ': + return true; + default: + return false; + } +} + +void strip_trailing_whitespace(s8 *str) { + u8 *lastchr = str->s + str->len - 1; + while (str->len > 0 && whitespace(*lastchr)) { + str->len--; + lastchr--; + } + (str->s)[str->len] = '\0'; +} + +/* + * Caller takes ownership of the data + */ +s8 enclose_word_in_s8_with(s8 str, s8 word, s8 prefix, s8 suffix) { + if (str.len == 0 || word.len == 0 || (prefix.len == 0 && suffix.len == 0)) + return str; + + assert(str.s[str.len] == '\0'); + stringbuilder_s sb = sb_init(str.len + 2 * (prefix.len + suffix.len)); + char *p = (char *)str.s; + char *prev = p; + while ((p = strstr(p, (char *)word.s)) != NULL) { + isize len = p - prev; + sb_append(&sb, (s8){.s = (u8 *)prev, .len = len}); + sb_append(&sb, prefix); + sb_append(&sb, word); + sb_append(&sb, suffix); + p += word.len; + prev = p; + } + sb_append(&sb, fromcstr_(prev)); + return sb_steals8(sb); +} + +void frees8(s8 *z) { + free(z->s); +} + +void frees8buffer(s8 *buf) { + while (buf_size(buf) > 0) + free(buf_pop(buf).s); + buf_free(buf); +} + +/* --------------- Start stringbuilder --------------- */ + +stringbuilder_s sb_init(size_t init_cap) { + return (stringbuilder_s){.len = 0, .cap = init_cap, .data = xcalloc(1, init_cap)}; +} + +void sb_append(stringbuilder_s *b, s8 str) { + assert(b->cap > 0); + if (b->cap < b->len + str.len) { + while (b->cap < b->len + str.len) { + b->cap *= 2; + assume(b->cap > 0); // Integer overflow check + } + + b->data = xrealloc(b->data, b->cap); + } + + for (int i = 0; i < str.len; i++) + b->data[b->len + i] = str.s[i]; + b->len += str.len; +} + +void sb_append_char(stringbuilder_s *sb, char c) { + sb_append(sb, (s8){.s = (u8 *)&c, .len = 1}); +} + +/* + * Returns the corresponding s8 to @sb + */ +s8 sb_gets8(stringbuilder_s sb) { + return (s8){.s = sb.data, .len = sb.len}; +} + +/* + * Frees the string builder and returns the corresponding s8. + */ +s8 sb_steals8(stringbuilder_s sb) { + s8 ret = {0}; + ret.len = sb.len; + ret.s = (u8 *)sb_steal_str(&sb); + return ret; +} + +char *sb_steal_str(stringbuilder_s *sb) { + sb_append_char(sb, '\0'); + char *r = (char *)sb->data; + *sb = (stringbuilder_s){0}; + return r; +} + +void sb_set(stringbuilder_s *sb, s8 s) { + sb->len = 0; + sb_append(sb, s); +} + +void sb_free(stringbuilder_s *sb) { + free(sb->data); + *sb = (stringbuilder_s){0}; +} +/* --------------- End stringbuilder --------------- */ + + +void substrremove(char *str, const s8 sub) { + assert(sub.s[sub.len] == '\0'); + + if (sub.len > 0) { + char *p = str; + while ((p = strstr(p, (char *)sub.s)) != NULL) { + memmove(p, p + sub.len, strlen(p + sub.len) + 1); + } + } +} + +// TODO: Cleaner (and non-null terminated) implementation +void nuke_whitespace(s8 *z) { + substrremove((char *)z->s, S("\n")); + substrremove((char *)z->s, S("\t")); + substrremove((char *)z->s, S(" ")); + substrremove((char *)z->s, S(" ")); + + *z = fromcstr_((char *)z->s); +} \ No newline at end of file diff --git a/src/utils/utf8.c b/src/utils/utf8.c index 2dee4a3..d051b5b 100644 --- a/src/utils/utf8.c +++ b/src/utils/utf8.c @@ -1,8 +1,10 @@ #include -#include "messages.h" -#include "utils/util.h" +#include "utils/messages.h" #include "utils/utf8.h" +#include "utils/util.h" + +#include const u8 utf8_chr_len_data[] = { /* 0XXX */ 1, 1, 1, 1, 1, 1, 1, 1, @@ -12,18 +14,19 @@ const u8 utf8_chr_len_data[] = { /* 1111 */ 4, /* maybe, but also could be invalid */ }; -s8 convert_to_utf8(char *str) { +s8 convertToUtf8(char *str) { g_autoptr(GError) error = NULL; s8 ret = fromcstr_(g_locale_to_utf8(str, -1, NULL, NULL, &error)); die_on(error, "Converting to UTF-8: %s", error->message); return ret; } -s8 striputf8chr(s8 s) { +void _nonnull_ stripUtf8Char(s8 s[static 1]) { + assert(s->len > 0); + + s->len--; // 0x80 = 10000000; 0xC0 = 11000000 - assert(s.len > 0); - s.len--; - while (s.len > 0 && (s.s[s.len] & 0x80) != 0x00 && (s.s[s.len] & 0xC0) != 0xC0) - s.len--; - return s; + while (s->len > 0 && (s->s[s->len] & 0x80) != 0x00 && (s->s[s->len] & 0xC0) != 0xC0) + s->len--; + s->s[s->len] = '\0'; } \ No newline at end of file diff --git a/src/utils/util.c b/src/utils/util.c index 921410e..3ce4c8a 100644 --- a/src/utils/util.c +++ b/src/utils/util.c @@ -19,348 +19,6 @@ void *xrealloc(void *ptr, size_t nbytes) { return p; } -/* --------------- Start s8 utils -------------- */ -i32 u8compare(u8 *restrict a, u8 *restrict b, isize n) { - for (; n; n--) { - i32 d = *a++ - *b++; - if (d) - return d; - } - return 0; -} - -/* - * Copy src into dst returning the remaining portion of dst. - */ -s8 s8copy(s8 dst, s8 src) { - assert(dst.len >= src.len && src.len >= 0); - - if (src.s && dst.s) { // Passing null to memcpy is undefined behaviour ... - memcpy(dst.s, src.s, src.len); - dst.s += src.len; - dst.len -= src.len; - } - return dst; -} - -s8 news8(isize len) { - assert(len >= 0); - return (s8){.s = new (u8, len + 1), // Include NULL terminator - .len = len}; -} - -s8 s8dup(s8 src) { -#ifdef UNIT_TEST - if (!src.s) - return (s8){0}; -#endif - - s8 r = news8(src.len); - s8copy(r, src); - return r; -} - -s8 fromcstr_(char *z) { - s8 s = {0}; - s.s = (u8 *)z; - s.len = z ? strlen(z) : 0; - return s; -} - -i32 s8equals(s8 a, s8 b) { - return a.len == b.len && !u8compare(a.s, b.s, a.len); -} - -s8 cuthead(s8 s, isize off) { - assert(off >= 0); - assert(off <= s.len); - s.s += off; - s.len -= off; - return s; -} - -s8 takehead(s8 s, isize len) { - assert(len >= 0); - assert(len <= s.len); - s.len = len; - return s; -} - -s8 cuttail(s8 s, isize len) { - assert(len >= 0); - assert(len <= s.len); - s.len -= len; - return s; -} - -s8 taketail(s8 s, isize len) { - return cuthead(s, s.len - len); -} - -b32 startswith(s8 s, s8 prefix) { - return s.len >= prefix.len && s8equals(takehead(s, prefix.len), prefix); -} - -b32 endswith(s8 s, s8 suffix) { - return s.len >= suffix.len && s8equals(taketail(s, suffix.len), suffix); -} - -s8 unescape(s8 str) { - isize s = 0, e = 0; - while (e < str.len) { - if (str.s[e] == '\\' && e + 1 < str.len) { - switch (str.s[e + 1]) { - case 'n': - str.s[s++] = '\n'; - e += 2; - break; - case '\\': - str.s[s++] = '\\'; - e += 2; - break; - case '"': - str.s[s++] = '"'; - e += 2; - break; - case 'r': - str.s[s++] = '\r'; - e += 2; - break; - default: - str.s[s++] = str.s[e++]; - } - } else - str.s[s++] = str.s[e++]; - } - str.len = s; - - return str; -} - -s8 concat_(s8 *strings) { - isize len = 0; - for (s8 *s = strings; !s8equals(*s, S8_STOPPER); s++) - len += s->len; - - s8 ret = news8(len); - s8 p = ret; - for (s8 *s = strings; !s8equals(*s, S8_STOPPER); s++) - p = s8copy(p, *s); - return ret; -} - -// TODO: Don't append sep when already there -s8 buildpath_(s8 *pathcomps) { -#ifdef _WIN32 - s8 sep = S("\\"); -#else - s8 sep = S("/"); -#endif - isize pathlen = 0; - bool first = true; - for (s8 *pc = pathcomps; !s8equals(*pc, S8_STOPPER); pc++) { - if (!first) - pathlen += sep.len; - pathlen += pc->len; - - first = false; - } - - s8 retpath = news8(pathlen); - s8 p = retpath; - - first = true; - for (s8 *pc = pathcomps; !s8equals(*pc, S8_STOPPER); pc++) { - if (!first) - p = s8copy(p, sep); - p = s8copy(p, *pc); - - first = false; - } - - return retpath; -} - -static b32 whitespace(u8 c) { - switch (c) { - case '\t': - case '\n': - case '\b': - case '\f': - case '\r': - case ' ': - return 1; - default: - return 0; - } -} - -void strip_trailing_whitespace(s8 *str) { - u8 *lastchr = str->s + str->len - 1; - while (str->len > 0 && whitespace(*lastchr)) { - str->len--; - lastchr--; - } - (str->s)[str->len] = '\0'; -} - -/* - * Caller takes ownership of the data - */ -s8 enclose_word_in_s8_with(s8 str, s8 word, s8 prefix, s8 suffix) { - if (str.len == 0 || word.len == 0 || (prefix.len == 0 && suffix.len == 0)) - return str; - - assert(str.s[str.len] == '\0'); - stringbuilder_s sb = sb_init(str.len + 2 * (prefix.len + suffix.len)); - char *p = (char *)str.s; - char *prev = p; - while ((p = strstr(p, (char *)word.s)) != NULL) { - isize len = p - prev; - sb_append(&sb, (s8){.s = (u8 *)prev, .len = len}); - sb_append(&sb, prefix); - sb_append(&sb, word); - sb_append(&sb, suffix); - p += word.len; - prev = p; - } - sb_append(&sb, fromcstr_(prev)); - return sb_steals8(sb); -} - -void frees8(s8 *z) { - free(z->s); -} - -void frees8buffer(s8 *buf) { - while (buf_size(buf) > 0) - free(buf_pop(buf).s); - buf_free(buf); -} -/* -------------- End s8 ---------------- */ - -/* --------------- Start stringbuilder --------------- */ - -stringbuilder_s sb_init(size_t init_cap) { - return (stringbuilder_s){.len = 0, .cap = init_cap, .data = xcalloc(1, init_cap)}; -} - -void sb_append(stringbuilder_s *b, s8 str) { - assert(b->cap > 0); - if (b->cap < b->len + str.len) { - while (b->cap < b->len + str.len) { - b->cap *= 2; - assume(b->cap > 0); // Integer overflow check - } - - b->data = xrealloc(b->data, b->cap); - } - - for (int i = 0; i < str.len; i++) - b->data[b->len + i] = str.s[i]; - b->len += str.len; -} - -void sb_append_char(stringbuilder_s *sb, char c) { - sb_append(sb, (s8){.s = (u8 *)&c, .len = 1}); -} - -/* - * Returns the corresponding s8 to @sb - */ -s8 sb_gets8(stringbuilder_s sb) { - return (s8){.s = sb.data, .len = sb.len}; -} - -/* - * Frees the string builder and returns the corresponding s8. - */ -s8 sb_steals8(stringbuilder_s sb) { - s8 ret = {0}; - ret.len = sb.len; - ret.s = (u8 *)sb_steal_str(&sb); - return ret; -} - -char *sb_steal_str(stringbuilder_s *sb) { - sb_append_char(sb, '\0'); - char *r = (char *)sb->data; - *sb = (stringbuilder_s){0}; - return r; -} - -void sb_set(stringbuilder_s *sb, s8 s) { - sb->len = 0; - sb_append(sb, s); -} - -void sb_free(stringbuilder_s *sb) { - free(sb->data); - *sb = (stringbuilder_s){0}; -} -/* --------------- End stringbuilder --------------- */ - -/* -------------- Start dictentry / dictionary utils ---------------- */ - -dictentry dictentry_dup(dictentry de) { - return (dictentry){.dictname = s8dup(de.dictname), - .kanji = s8dup(de.kanji), - .reading = s8dup(de.reading), - .definition = s8dup(de.definition)}; -} - -void dictentry_print(dictentry de) { - printf("dictname: %s\n" - "kanji: %s\n" - "reading: %s\n" - "definition: %s\n", - de.dictname.s, de.kanji.s, de.reading.s, de.definition.s); -} - -void dictionary_add(dictentry **dict, dictentry de) { - buf_push(*dict, de); -} - -isize dictlen(dictentry *dict) { - return buf_size(dict); -} - -void dictentry_free(dictentry de) { - frees8(&de.dictname); - frees8(&de.kanji); - frees8(&de.reading); - frees8(&de.definition); -} - -void dictionary_free(dictentry **dict) { - while (buf_size(*dict) > 0) - dictentry_free(buf_pop(*dict)); - buf_free(*dict); -} - -dictentry dictentry_at_index(dictentry *dict, isize index) { - assert(index >= 0 && (size_t)index < buf_size(dict)); - return dict[index]; -} - -dictentry *pointer_to_entry_at(dictentry *dict, isize index) { - assert(index >= 0 && (size_t)index < buf_size(dict)); - return dict + index; -} -/* -------------- End dictentry / dictionary ---------------- */ - -/* -------------- Start freqentry ---------------- */ -freqentry freqentry_dup(freqentry fe) { - return (freqentry){ - .word = s8dup(fe.word), .reading = s8dup(fe.reading), .frequency = fe.frequency}; -} - -void freqentry_free(freqentry *fe) { - frees8(&fe->word); - frees8(&fe->reading); -} -/* -------------- end freqentry ---------------- */ - /** * Performs safe, bounded string formatting into a buffer. On error or * truncation, assume() aborts. @@ -373,14 +31,3 @@ size_t snprintf_safe(char *buf, size_t len, const char *fmt, ...) { assume(needed >= 0 && (size_t)needed < len); return (size_t)needed; } - -void substrremove(char *str, const s8 sub) { - assert(sub.s[sub.len] == '\0'); - - if (sub.len > 0) { - char *p = str; - while ((p = strstr(p, (char *)sub.s)) != NULL) { - memmove(p, p + sub.len, strlen(p + sub.len) + 1); - } - } -} diff --git a/src/yomichan_parser.c b/src/yomichan_parser.c index 0c6e6ff..bfc6f16 100644 --- a/src/yomichan_parser.c +++ b/src/yomichan_parser.c @@ -1,11 +1,16 @@ #include "yomichan_parser.h" +#include "objects/freqentry.h" + #include -#include "messages.h" +#include "utils/messages.h" +#include "utils/str.h" #include "utils/util.h" #include "utils/yyjson.h" +#include + DEFINE_DROP_FUNC(struct zip_file *, zip_fclose) DEFINE_DROP_FUNC(yyjson_doc *, yyjson_doc_free) diff --git a/tests/ankiconnect_tests.c b/tests/ankiconnect_tests.c index bf044c2..b46715f 100644 --- a/tests/ankiconnect_tests.c +++ b/tests/ankiconnect_tests.c @@ -9,7 +9,7 @@ TestSuite *ankiconnect_tests(void); static retval_s sendRequest(s8 request, ResponseFunc response_checker) { mock(request.s); - return (retval_s){0}; + return (retval_s){.ok=true}; } Describe(AnkiConnectC); @@ -19,8 +19,8 @@ AfterEach(AnkiConnectC) { } Ensure(AnkiConnectC, sends_correct_guiBrowse_request) { - char *expected = - "{ \"action\": \"guiBrowse\", \"version\": 6, \"params\": { \"query\" : \"\\\"deck:Japanese\\\" \\\"VocabKanji:面白い\\\"\" } }"; + char *expected = "{ \"action\": \"guiBrowse\", \"version\": 6, \"params\": { \"query\" : " + "\"\\\"deck:Japanese\\\" \\\"VocabKanji:面白い\\\"\" } }"; expect(sendRequest, when(request.s, is_equal_to_string(expected))); @@ -29,16 +29,50 @@ Ensure(AnkiConnectC, sends_correct_guiBrowse_request) { Ensure(AnkiConnectC, sends_correct_search_request_without_suspended) { _drop_(frees8) s8 received = form_search_req(false, true, "Japanese", "VocabKanji", "敷衍"); - s8 expected = S( - "{ \"action\": \"findCards\", \"version\": 6, \"params\": { \"query\" : \"\\\"deck:Japanese\\\" \\\"VocabKanji:敷衍\\\" -is:suspended\" } }"); - assert_that(received.s, is_equal_to_string((const char *)expected.s)); + const char* expected = "{ \"action\": \"findCards\", \"version\": 6, \"params\": { \"query\" : " + "\"\\\"deck:Japanese\\\" \\\"VocabKanji:敷衍\\\" -is:suspended\" } }"; + assert_that(received.s, is_equal_to_string(expected)); } Ensure(AnkiConnectC, sends_correct_search_request_without_new) { - _drop_(frees8) s8 received = form_search_req(true, false, "Japanese", "VocabKanji", "敷衍"); - s8 expected = S( - "{ \"action\": \"findCards\", \"version\": 6, \"params\": { \"query\" : \"\\\"deck:Japanese\\\" \\\"VocabKanji:敷衍\\\" -is:new\" } }"); - assert_that(received.s, is_equal_to_string((const char *)expected.s)); + _drop_(frees8) s8 received = form_search_req(true, false, "Japanese", "VocabKanji", "怒涛"); + const char *expected = "{ \"action\": \"findCards\", \"version\": 6, \"params\": { \"query\" : " + "\"\\\"deck:Japanese\\\" \\\"VocabKanji:怒涛\\\" -is:new\" } }"; + assert_that(received.s, is_equal_to_string(expected)); +} + +Ensure(AnkiConnectC, sends_correct_search_request_without_new_and_suspended) { + _drop_(frees8) s8 received = form_search_req(false, false, "Japanese", "VocabKanji", "寿命"); + const char *expected = "{ \"action\": \"findCards\", \"version\": 6, \"params\": { \"query\" : " + "\"\\\"deck:Japanese\\\" \\\"VocabKanji:寿命\\\" -is:suspended -is:new\" } }"; + assert_that(received.s, is_equal_to_string(expected)); +} + +Ensure(AnkiConnectC, sends_correct_addNote_request) { + char *fieldNames[] = {"SentKanji", "VocabKanji", "VocabFurigana", "VocabDef", "Notes"}; + char *fieldEntries[] = { + "白鯨同様、世界中が被害を被っている。騎士団も長く辛酸を味わわされてきた相手だ", "辛酸", + "辛酸[しんさん]", "つらい思い。苦しみ。", "Re:Zero"}; + char *tags[] = {"someTag", NULL}; + ankicard ac = (ankicard){.deck = "Japanese", + .notetype = "Japanese Sentences", + .num_fields = arrlen(fieldNames), + .fieldnames = fieldNames, + .fieldentries = fieldEntries, + .tags = tags}; + const char *expected = + "{\"action\": \"addNote\",\"version\": 6,\"params\": {\"note\": {\"deckName\": " + "\"Japanese\",\"modelName\": \"Japanese Sentences\",\"fields\": {\"SentKanji\" : " + "\"白鯨同様、世界中が被害を被っている。騎士団も長く辛酸を味わわされてきた相手だ\"," + "\"VocabKanji\" : \"辛酸\",\"VocabFurigana\" : \"辛酸[しんさん]\",\"VocabDef\" : " + "\"つらい思い。苦しみ。\",\"Notes\" : \"Re:Zero\"},\"options\": {\"allowDuplicate\": " + "true},\"tags\": [\"someTag\"]}}}"; + + expect(sendRequest, when(request.s, is_equal_to_string(expected))); + + char *error = NULL; + + ac_addNote(ac, &error); } Ensure(AnkiConnectC, json_escapes_ankicard) { @@ -73,6 +107,8 @@ TestSuite *ankiconnect_tests(void) { add_test_with_context(suite, AnkiConnectC, sends_correct_guiBrowse_request); add_test_with_context(suite, AnkiConnectC, sends_correct_search_request_without_suspended); add_test_with_context(suite, AnkiConnectC, sends_correct_search_request_without_new); + add_test_with_context(suite, AnkiConnectC, sends_correct_search_request_without_new_and_suspended); + add_test_with_context(suite, AnkiConnectC, sends_correct_addNote_request); add_test_with_context(suite, AnkiConnectC, json_escapes_ankicard); return suite; } diff --git a/tests/db_mock.c b/tests/db_mock.c index 6f0039b..489ad36 100644 --- a/tests/db_mock.c +++ b/tests/db_mock.c @@ -1,5 +1,8 @@ #include "db.h" +#include "utils/dict.h" +#include "utils/str.h" + #include struct database_s { @@ -7,29 +10,30 @@ struct database_s { }; database_t *db_open(char *dbpath, bool readonly) { - mock(dbpath, readonly); return new(database_t, 1); } void db_close(database_t *db) { - mock(db); + free(db); } void db_put_dictent(database_t *db, dictentry de) { mock(db, de.definition.s, de.dictname.s, de.kanji.s, de.reading.s, de.frequency); } -void db_get_dictents(const database_t *db, s8 headword, dictentry *dict[static 1]) { - mock(db, headword.s, dict); +void db_add_dictents(const database_t *db, s8 headword, dictentry *dict[static 1]) { + if (s8equals(headword, S("世界"))) { + dictionary_add(dict, (dictentry){ .kanji = S("世界") }); + } + // mock(db, headword.s, dict); } void db_put_freq(const database_t *db, freqentry fe) { mock(db, fe.frequency, fe.reading.s, fe.word.s); } -i32 db_check_exists(s8 dbpath) { - mock(dbpath.s); - return 1; // TODO: how to mock? +bool db_check_exists(s8 dbpath) { + return (bool)mock(dbpath.s); } void db_remove(s8 dbpath) { diff --git a/tests/dictpopup_tests.c b/tests/dictpopup_tests.c index 4a5424c..5fc1799 100644 --- a/tests/dictpopup_tests.c +++ b/tests/dictpopup_tests.c @@ -15,6 +15,7 @@ s8 get_windowname(void) { } void file_copy_sync(const char *source_path, const char *dest_path) { + mock(source_path, dest_path); return; } @@ -23,7 +24,15 @@ void createdir(char *dirpath) { } bool check_file_exists(const char *fn) { - return true; + return (bool)mock(fn); +} + +const char *get_user_data_dir(void) { + return "/home/user/.local/share"; +} + +char *get_next_clipboard(void) { + return (char *)mock(); } TestSuite *dictpopup_tests(void); @@ -35,7 +44,8 @@ AfterEach(DictPopup) { } Ensure(DictPopup, properly_prepares_ankicard) { - // s8 sent = S("白鯨同様、世界中が被害を被っている。騎士団も長く辛酸を味わわされてきた相手だ"); + char *sent = "白鯨同様、世界中が被害を被っている。騎士団も長く辛酸を味わわされてきた相手だ"; + expect(get_next_clipboard, will_return(sent)); s8 lookup = S("辛酸"); dictentry de = (dictentry){.dictname = S("明鏡国語辞典 第二版"), .kanji = S("辛酸"), @@ -50,10 +60,14 @@ Ensure(DictPopup, properly_prepares_ankicard) { .fieldnames = fieldNames, .deck = "Japanese", .notetype = "Japanese Sentences"}}; + cfg.anki.copySentence = true; ankicard ac = prepare_ankicard(lookup, de, config); - assert_that(ac.fieldentries[0], is_equal_to_string(0)); + assert_that( + ac.fieldentries[0], + is_equal_to_string( + "白鯨同様、世界中が被害を被っている。騎士団も長く辛酸を味わわされてきた相手だ")); assert_that(ac.fieldentries[1], is_equal_to_string("辛酸")); assert_that(ac.fieldentries[2], is_equal_to_string("辛酸[しんさん]")); assert_that(ac.fieldentries[3], is_equal_to_string("つらい思い。苦しみ。")); @@ -62,8 +76,42 @@ Ensure(DictPopup, properly_prepares_ankicard) { free(ac.fieldentries); } +Ensure(DictPopup, looks_up_substrings_as_expected) { + _drop_(frees8) s8 lookup = s8dup(S("世界からはいつの間にか風の音が、虫の鳴き声が消失していた")); + _drop_(db_close) database_t *db = db_open("dummy", true); + + dictentry *dict = lookup_first_matching_prefix(&lookup, db); + + assert_that(dict[0].kanji.s, is_equal_to_string("世界")); + + buf_free(dict); +} + +Ensure(DictPopup, copies_default_database_if_no_database_exists) { + _drop_(free) s8 dbdir = buildpath(fromcstr_((char *)get_user_data_dir()), S("dictpopup")); + _drop_(free) s8 dbfile = buildpath(dbdir, S("data.mdb")); + + always_expect(check_file_exists, will_return(true)); + always_expect(db_check_exists, will_return(false)); + + expect(file_copy_sync, when(source_path, is_equal_to_string(DEFAULT_DATABASE_LOCATIONS[0])), + when(dest_path, is_equal_to_string((char *)dbfile.s))); + + // expect(read_user_settings, ) + // cfg.general.dbDir = (char*)dbdir.s; + dictpopup_init(); +} + +Ensure(DictPopup, sorts_direct_matches_first) { + //TODO + assert(true); +} + TestSuite *dictpopup_tests(void) { TestSuite *suite = create_test_suite(); add_test_with_context(suite, DictPopup, properly_prepares_ankicard); + add_test_with_context(suite, DictPopup, looks_up_substrings_as_expected); + add_test_with_context(suite, DictPopup, sorts_direct_matches_first); + // add_test_with_context(suite, DictPopup, copies_default_database_if_no_database_exists); return suite; } diff --git a/tests/jppron_tests/jppron_tests.c b/tests/jppron_tests/jppron_tests.c new file mode 100644 index 0000000..23d77c3 --- /dev/null +++ b/tests/jppron_tests/jppron_tests.c @@ -0,0 +1,58 @@ +#include +#include + +#include "jppron/jppron.c" + +TestSuite *jppron_tests(void); + +Describe(Jppron); +BeforeEach(Jppron) { +} +AfterEach(Jppron) { +} + +database *jdb_open(char *path, bool readonly) { + return NULL; +} + +void jdb_close(database *db) { + return; +} + +void jdb_add_headword_with_file(database *db, s8 headword, s8 filepath) { + mock(headword.s, filepath.s); +} + +void jdb_add_file_with_fileinfo(database *db, s8 filepath, fileinfo_s fi) { + mock(filepath.s, fi.hira_reading.s, fi.origin.s, fi.pitch_number.s, fi.pitch_pattern.s); +} + +s8 *jdb_get_files(database *db, s8 key) { + return (s8*)mock(key.s); +} + +fileinfo_s jdb_get_fileinfo(database *db, s8 fullpath) { + return (fileinfo_s){0}; +} + +i32 jdb_check_exists(s8 dbpath) { + return (i32)mock(dbpath.s); +} + +void jdb_remove(s8 dbpath) { + mock(dbpath.s); +} + +void play_audio(s8 filepath) { + mock(filepath.s); +} + +Ensure(Jppron, excludes_trailing_o) { + assert(true); +} + +TestSuite *jppron_tests(void) { + TestSuite *suite = create_test_suite(); + add_test_with_context(suite, Jppron, excludes_trailing_o); + return suite; +} \ No newline at end of file diff --git a/tests/main.c b/tests/main.c index 384ad50..1aa0cf2 100644 --- a/tests/main.c +++ b/tests/main.c @@ -6,6 +6,7 @@ TestSuite *dictpopup_tests(void); TestSuite *yomichan_parser_tests(void); TestSuite *ajt_audio_index_parser_tests(void); TestSuite *s8_tests(void); +TestSuite *jppron_tests(void); int main(int argc, char **argv) { TestSuite *suite = create_test_suite(); @@ -15,6 +16,7 @@ int main(int argc, char **argv) { add_suite(suite, ajt_audio_index_parser_tests()); add_suite(suite, s8_tests()); add_suite(suite, dictpopup_tests()); + add_suite(suite, jppron_tests()); if (argc > 1) { return run_single_test(suite, argv[1], create_text_reporter());