feat: watch: try pager mode..
This commit is contained in:
@@ -25,7 +25,7 @@ stdenv.mkDerivation {
|
|||||||
'';
|
'';
|
||||||
|
|
||||||
meta = {
|
meta = {
|
||||||
description = "hectic";
|
description = "hmpl";
|
||||||
license = lib.licenses.mit;
|
license = lib.licenses.mit;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,154 +66,116 @@ void hmpl_render_interpolation_tags(Arena *arena, char **text_ptr, const Json *
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CREATE OR REPLACE FUNCTION common.render_template_loop_blocks(result TEXT, context JSONB)
|
// {{item#array}}...{{/array}}
|
||||||
// RETURNS TEXT LANGUAGE plpgsql AS $$
|
void hmpl_render_section_tags(Arena *arena, char **text_ptr, Json *context, const char * const prefix_start, const char * const prefix_end, const char * const separator_pattern) {
|
||||||
// DECLARE
|
|
||||||
// loop_start INT;
|
|
||||||
// key_end INT;
|
|
||||||
// loop_end INT;
|
|
||||||
// loop_key TEXT;
|
|
||||||
// block TEXT;
|
|
||||||
// rendered_block TEXT;
|
|
||||||
// arr JSONB;
|
|
||||||
// item JSONB;
|
|
||||||
// item_text TEXT;
|
|
||||||
// BEGIN
|
|
||||||
// LOOP
|
|
||||||
// loop_start := strpos(result, '{{#');
|
|
||||||
// EXIT WHEN loop_start = 0; -- Exit if no loop start found.
|
|
||||||
//
|
|
||||||
// -- Locate the end of the loop key marker.
|
|
||||||
// key_end := strpos(result, '}}', loop_start);
|
|
||||||
// IF key_end = 0 THEN
|
|
||||||
// RAISE EXCEPTION 'Malformed template: missing closing braces for loop start';
|
|
||||||
// END IF;
|
|
||||||
//
|
|
||||||
// -- Extract the key used for the loop.
|
|
||||||
// loop_key := substr_cloneing(result from loop_start + 3 for key_end - loop_start - 3);
|
|
||||||
//
|
|
||||||
// RAISE DEBUG 'loop key %', loop_key;
|
|
||||||
//
|
|
||||||
// -- Find the matching loop end marker for this key.
|
|
||||||
// loop_end := strpos(result, '{{/#' || loop_key || '}}', key_end);
|
|
||||||
// IF loop_end = 0 THEN
|
|
||||||
// RAISE EXCEPTION 'Malformed template: missing loop end for key %', loop_key;
|
|
||||||
// END IF;
|
|
||||||
//
|
|
||||||
// -- Extract the inner block of the loop.
|
|
||||||
// block := substr_cloneing(result from key_end + 2 for loop_end - key_end - 2);
|
|
||||||
//
|
|
||||||
// -- Retrieve the JSON array from the context for the loop key.
|
|
||||||
// arr := eval_value(context, loop_key);
|
|
||||||
// rendered_block := '';
|
|
||||||
//
|
|
||||||
// -- If an array is found, iterate over each element.
|
|
||||||
// IF arr IS NOT NULL AND jsonb_typeof(arr) = 'array' THEN
|
|
||||||
// FOR item IN SELECT * FROM jsonb_array_elements(arr) LOOP
|
|
||||||
// item_text := block; -- Begin with the raw block.
|
|
||||||
// IF jsonb_typeof(item) != 'object' THEN
|
|
||||||
// -- Replace interpolation for primitive values.
|
|
||||||
// item_text := replace(item_text, '{{.}}', item::text);
|
|
||||||
// ELSE
|
|
||||||
// -- For object values, iterate over each key/value.
|
|
||||||
// item_text := render_template_interpolations(item_text, item, '.'::CHAR(1));
|
|
||||||
// item_text := render_template_conditions(item_text, item, '.');
|
|
||||||
// END IF;
|
|
||||||
// rendered_block := rendered_block || item_text;
|
|
||||||
// END LOOP;
|
|
||||||
// END IF;
|
|
||||||
//
|
|
||||||
// -- Replace the entire loop block in the result with the rendered content.
|
|
||||||
// result := substr_cloneing(result from 1 for loop_start - 1)
|
|
||||||
// || rendered_block
|
|
||||||
// || substr_cloneing(result from loop_end + char_length('{{/#' || loop_key || '}}'));
|
|
||||||
// END LOOP;
|
|
||||||
//
|
|
||||||
// RETURN result;
|
|
||||||
// END $$;
|
|
||||||
|
|
||||||
// {{#array_key}}
|
|
||||||
void hmpl_render_section_tags(Arena *arena, char **text_ptr, Json *context, const char * const prefix_start, const char * const prefix_end, const char * const separator_pattern){
|
|
||||||
raise_debug("hmpl_render_section_tags(%p, %s, <optimized>, %s, %s, %s)", arena, *text_ptr, prefix_start, prefix_end, separator_pattern);
|
raise_debug("hmpl_render_section_tags(%p, %s, <optimized>, %s, %s, %s)", arena, *text_ptr, prefix_start, prefix_end, separator_pattern);
|
||||||
|
|
||||||
|
// Create search patterns
|
||||||
char start_pattern[32];
|
char start_pattern[32];
|
||||||
snprintf(start_pattern, sizeof(start_pattern), "{{%s", prefix_start);
|
snprintf(start_pattern, sizeof(start_pattern), "{{%s", prefix_start);
|
||||||
int start_pattern_length = strlen(start_pattern);
|
Slice start_slice = slice_create(char, start_pattern, strlen(start_pattern), 0, strlen(start_pattern));
|
||||||
|
|
||||||
// TODO: rename close_tag_start_pattern
|
// Create a mutable copy of separator_pattern
|
||||||
char end_pattern[32];
|
char separator_copy[32];
|
||||||
snprintf(end_pattern, sizeof(end_pattern), "{{%s", prefix_end);
|
strncpy(separator_copy, separator_pattern, sizeof(separator_copy) - 1);
|
||||||
int end_pattern_length = strlen(end_pattern);
|
separator_copy[sizeof(separator_copy) - 1] = '\0';
|
||||||
|
Slice separator_slice = slice_create(char, separator_copy, strlen(separator_copy), 0, strlen(separator_copy));
|
||||||
int separator_pattern_length = strlen(separator_pattern);
|
if (separator_slice.len == 0) {
|
||||||
if (!separator_pattern || separator_pattern_length == 0) {
|
|
||||||
raise_exception("Unexpected usage: separator pattern cannot be empty");
|
raise_exception("Unexpected usage: separator pattern cannot be empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
int offset = 0;
|
// Create slice for the text
|
||||||
|
Slice text_slice = slice_create(char, *text_ptr, strlen(*text_ptr), 0, strlen(*text_ptr));
|
||||||
|
size_t offset = 0;
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
char *current_text = *text_ptr;
|
// Find tag start
|
||||||
char *opening_tag_start = strstr(current_text + offset, start_pattern);
|
char *text_data = (char*)text_slice.data;
|
||||||
if (!opening_tag_start)
|
char *opening_tag_start = strstr(text_data + offset, (char*)start_slice.data);
|
||||||
break;
|
if (!opening_tag_start) break;
|
||||||
int start_index = opening_tag_start - current_text;
|
|
||||||
int relative_key_start = start_index + start_pattern_length;
|
|
||||||
|
|
||||||
char *opening_tag_separator = strstr(opening_tag_start, separator_pattern);
|
// Create slice for separator search
|
||||||
if (!opening_tag_start) {
|
size_t start_index = opening_tag_start - text_data;
|
||||||
raise_exception("Malformed template: missing separator for section tag or not specifiet name for element");
|
Slice remaining_slice = slice_subslice(text_slice, start_index, text_slice.len - start_index);
|
||||||
|
|
||||||
|
// Find separator
|
||||||
|
char *opening_tag_separator = strstr((char*)remaining_slice.data, (char*)separator_slice.data);
|
||||||
|
if (!opening_tag_separator) {
|
||||||
|
raise_exception("Malformed template: missing separator for section tag or not specified name for element");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
int separator_index = opening_tag_separator - current_text;
|
|
||||||
int element_name_start = separator_index + separator_pattern_length;
|
|
||||||
|
|
||||||
char *opening_tag_end = strstr(opening_tag_separator, "}}");
|
// Extract element name (now before separator)
|
||||||
|
size_t separator_index = opening_tag_separator - (char*)remaining_slice.data;
|
||||||
|
size_t element_name_start = start_slice.len;
|
||||||
|
size_t element_name_length = separator_index;
|
||||||
|
|
||||||
|
char *element_name = arena_alloc(arena, element_name_length + 1);
|
||||||
|
substr_clone((char*)remaining_slice.data, element_name, element_name_start, element_name_length);
|
||||||
|
element_name[element_name_length] = '\0';
|
||||||
|
|
||||||
|
// Find closing braces
|
||||||
|
Slice after_separator = slice_subslice(remaining_slice, separator_index + separator_slice.len,
|
||||||
|
remaining_slice.len - separator_index - separator_slice.len);
|
||||||
|
char *opening_tag_end = strstr((char*)after_separator.data, "}}");
|
||||||
if (!opening_tag_end) {
|
if (!opening_tag_end) {
|
||||||
raise_exception("Malformed template: missing closing braces for section tag");
|
raise_exception("Malformed template: missing closing braces for section tag");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
assert((size_t)opening_tag_end > (size_t)opening_tag_separator);
|
|
||||||
assert((size_t)opening_tag_separator > (size_t)opening_tag_start);
|
|
||||||
|
|
||||||
int key_length = (opening_tag_separator - current_text) - relative_key_start;
|
|
||||||
assert(key_length > 0);
|
|
||||||
|
|
||||||
|
// Extract key (now after separator)
|
||||||
|
size_t key_start = 0;
|
||||||
|
size_t key_length = opening_tag_end - (char*)after_separator.data;
|
||||||
char *key = arena_alloc(arena, key_length + 1);
|
char *key = arena_alloc(arena, key_length + 1);
|
||||||
substr_clone(current_text, key, relative_key_start, key_length);
|
substr_clone((char*)after_separator.data, key, key_start, key_length);
|
||||||
|
key[key_length] = '\0';
|
||||||
|
|
||||||
int element_name_length = (opening_tag_end - current_text) - element_name_start;
|
// Create pattern for closing tag
|
||||||
assert(element_name_length > 0);
|
char *close_tag_pattern = arena_alloc(arena, start_slice.len + key_length + 3); // +3 for "{{" and "}}"
|
||||||
|
snprintf(close_tag_pattern, start_slice.len + key_length + 3,
|
||||||
|
"{{%s%s}}", prefix_end, key);
|
||||||
|
|
||||||
char *element_name = arena_alloc(arena, element_name_length + 1);
|
// Find closing tag
|
||||||
substr_clone(current_text, element_name, element_name_start, element_name_length);
|
size_t after_opening_end = (opening_tag_end - (char*)after_separator.data) + 2;
|
||||||
|
Slice after_opening_slice = slice_subslice(after_separator, after_opening_end,
|
||||||
|
after_separator.len - after_opening_end);
|
||||||
|
|
||||||
int close_tag_patern_length = start_pattern_length + key_length + end_pattern_length;
|
// Find the exact closing tag by checking for complete tag pattern
|
||||||
char *close_tag_patern = arena_alloc(arena, close_tag_patern_length + 1);
|
char *close_tag = NULL;
|
||||||
snprintf(close_tag_patern, sizeof(*close_tag_patern), "%s%s%s", start_pattern, key, end_pattern);
|
char *search_start = (char*)after_opening_slice.data;
|
||||||
|
while ((search_start = strstr(search_start, "{{")) != NULL) {
|
||||||
|
if (strncmp(search_start, close_tag_pattern, strlen(close_tag_pattern)) == 0) {
|
||||||
|
close_tag = search_start;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
search_start += 2; // Move past the "{{" we found
|
||||||
|
}
|
||||||
|
|
||||||
char *close_tag = strstr(opening_tag_end + offset + 1, close_tag_patern);
|
|
||||||
if (!close_tag) {
|
if (!close_tag) {
|
||||||
raise_exception("Malformed template: missing loop end for key %s", key);
|
raise_exception("Malformed template: missing loop end for key %s", key);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get array from context
|
||||||
Json *arr = eval_object(arena, context, key);
|
Json *arr = eval_object(arena, context, key);
|
||||||
|
|
||||||
if (arr && arr->type == JSON_ARRAY) {
|
if (arr && arr->type == JSON_ARRAY) {
|
||||||
|
// Count array elements
|
||||||
size_t elem_count = 0;
|
size_t elem_count = 0;
|
||||||
for (Json *e = arr->child; e; e = e->next) elem_count++;
|
for (Json *e = arr->child; e; e = e->next) elem_count++;
|
||||||
|
|
||||||
|
// Allocate memory for replacement
|
||||||
char *replacement = arena_alloc(arena, MEM_KiB * elem_count);
|
char *replacement = arena_alloc(arena, MEM_KiB * elem_count);
|
||||||
size_t offset = 0;
|
size_t replacement_offset = 0;
|
||||||
|
|
||||||
char *block_buff = arena_alloc(arena, MEM_KiB);
|
// Extract template block
|
||||||
size_t relative_block_start = (size_t)opening_tag_end + 2 - (size_t)current_text;
|
size_t block_start = after_opening_end;
|
||||||
raise_trace("relative_block_start: %p = %p - 2 - %p", opening_tag_end, current_text);
|
size_t block_length = (close_tag - (char*)after_opening_slice.data);
|
||||||
size_t block_len = (size_t)opening_tag_end - (size_t)close_tag - 2;
|
char *block_buff = arena_alloc(arena, block_length + 1);
|
||||||
raise_trace("block_len %p = %p - %p - 2", block_len, opening_tag_end, close_tag);
|
substr_clone((char*)after_opening_slice.data, block_buff, block_start, block_length);
|
||||||
assert(block_len > 0);
|
block_buff[block_length] = '\0';
|
||||||
substr_clone(current_text, block_buff, relative_block_start, block_len);
|
|
||||||
|
|
||||||
|
// Process each array element
|
||||||
for (Json *elem = arr->child; elem; elem = elem->next) {
|
for (Json *elem = arr->child; elem; elem = elem->next) {
|
||||||
char *block = arena_strdup(arena, block_buff);
|
char *block = arena_strdup(arena, block_buff);
|
||||||
|
|
||||||
@@ -224,20 +186,26 @@ void hmpl_render_section_tags(Arena *arena, char **text_ptr, Json *context, cons
|
|||||||
raise_trace("block after: %s", block);
|
raise_trace("block after: %s", block);
|
||||||
|
|
||||||
size_t block_len = strlen(block);
|
size_t block_len = strlen(block);
|
||||||
memcpy(replacement + offset, block, block_len);
|
memcpy(replacement + replacement_offset, block, block_len);
|
||||||
offset += block_len;
|
replacement_offset += block_len;
|
||||||
}
|
}
|
||||||
|
|
||||||
replacement[offset] = '\0';
|
replacement[replacement_offset] = '\0';
|
||||||
raise_trace("replacement: %s", replacement);
|
raise_trace("replacement: %s", replacement);
|
||||||
|
|
||||||
char *new_text = arena_repstr(arena, current_text,
|
// Calculate replacement positions
|
||||||
(size_t)opening_tag_start - 1,
|
size_t replace_start = start_index;
|
||||||
close_tag + close_tag_patern_length - opening_tag_start + 2,
|
size_t replace_length = (close_tag - (char*)after_opening_slice.data) +
|
||||||
replacement);
|
start_slice.len + key_length + 2;
|
||||||
|
|
||||||
|
// Perform replacement
|
||||||
|
char *new_text = arena_repstr(arena, (char*)text_slice.data, replace_start, replace_length, replacement);
|
||||||
*text_ptr = new_text;
|
*text_ptr = new_text;
|
||||||
|
|
||||||
|
// Update text slice
|
||||||
|
text_slice = slice_create(char, new_text, strlen(new_text), 0, strlen(new_text));
|
||||||
}
|
}
|
||||||
|
|
||||||
offset = start_index;
|
offset = start_index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
# Usage: make.sh [build|check] [--norun] [--debug] [--color]
|
# Usage: make.sh [build|check] [--norun] [--debug] [--color]
|
||||||
# Options:
|
# Options:
|
||||||
# build Build the library and app (default if no mode is provided).
|
# build Build the library and app (default if no mode is provided).
|
||||||
|
# watch Build the library and app and watch for changes.
|
||||||
|
# run Build and run the app.
|
||||||
# check Build tests; runs them unless --norun is specified.
|
# check Build tests; runs them unless --norun is specified.
|
||||||
# --norun (check only) Build tests but do not run them.
|
# --norun (check only) Build tests but do not run them.
|
||||||
# --debug Build with -O0 (debug mode).
|
# --debug Build with -O0 (debug mode).
|
||||||
@@ -9,7 +11,7 @@
|
|||||||
# help, --help Show this help message.
|
# help, --help Show this help message.
|
||||||
|
|
||||||
check_dependencies() {
|
check_dependencies() {
|
||||||
for dep in cc ar; do
|
for dep in cc ar entr pager; do
|
||||||
if ! command -v "$dep" >/dev/null 2>&1; then
|
if ! command -v "$dep" >/dev/null 2>&1; then
|
||||||
echo "Error: Required dependency '$dep' not found." >&2
|
echo "Error: Required dependency '$dep' not found." >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -22,6 +24,8 @@ print_help() {
|
|||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: $0 [build|check] [--norun] [--debug] [--color]
|
Usage: $0 [build|check] [--norun] [--debug] [--color]
|
||||||
build Build the library and app (default).
|
build Build the library and app (default).
|
||||||
|
watch Build the library and app and watch for changes.
|
||||||
|
run Build and run the app.
|
||||||
check Build tests; runs them unless --norun is specified.
|
check Build tests; runs them unless --norun is specified.
|
||||||
--norun (check only) Build tests but do not run them.
|
--norun (check only) Build tests but do not run them.
|
||||||
--debug Build with debug flags (-O0).
|
--debug Build with debug flags (-O0).
|
||||||
@@ -74,8 +78,7 @@ if [ -n "$COLOR_FLAG" ]; then
|
|||||||
CFLAGS="$CFLAGS $COLOR_FLAG"
|
CFLAGS="$CFLAGS $COLOR_FLAG"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
case "$MODE" in
|
build() {
|
||||||
build)
|
|
||||||
mkdir -p target
|
mkdir -p target
|
||||||
echo "# Build library"
|
echo "# Build library"
|
||||||
# shellcheck disable=SC2086
|
# shellcheck disable=SC2086
|
||||||
@@ -85,10 +88,20 @@ case "$MODE" in
|
|||||||
echo "# Build app"
|
echo "# Build app"
|
||||||
# shellcheck disable=SC2086
|
# shellcheck disable=SC2086
|
||||||
cc $CFLAGS $OPTFLAGS main.c -Ltarget -lhmpl $LDFLAGS -o target/hmpl
|
cc $CFLAGS $OPTFLAGS main.c -Ltarget -lhmpl $LDFLAGS -o target/hmpl
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$MODE" in
|
||||||
|
watch)
|
||||||
|
entr -r sh ./make.sh build | pager
|
||||||
|
;;
|
||||||
|
build)
|
||||||
|
build
|
||||||
|
;;
|
||||||
|
run)
|
||||||
|
build && ./target/hmpl
|
||||||
;;
|
;;
|
||||||
check)
|
check)
|
||||||
mkdir -p target/test
|
mkdir -p target/test
|
||||||
export LOG_LEVEL=TRACE
|
|
||||||
for test_file in test/*.c; do
|
for test_file in test/*.c; do
|
||||||
exe="target/test/$(basename "${test_file%.c}")"
|
exe="target/test/$(basename "${test_file%.c}")"
|
||||||
# shellcheck disable=SC2086
|
# shellcheck disable=SC2086
|
||||||
|
|||||||
6063
package/c/hmpl/test.log
Normal file
6063
package/c/hmpl/test.log
Normal file
File diff suppressed because it is too large
Load Diff
@@ -101,7 +101,7 @@
|
|||||||
"}"
|
"}"
|
||||||
|
|
||||||
#define TEST_DATA_SIMPLE_SECTION_ITERATION_TEMPLATE \
|
#define TEST_DATA_SIMPLE_SECTION_ITERATION_TEMPLATE \
|
||||||
"{{#array element}}" \
|
"{{#element array}}" \
|
||||||
" {{element.field.subfield}}" \
|
" {{element.field.subfield}}" \
|
||||||
"{{/array}}"
|
"{{/array}}"
|
||||||
|
|
||||||
@@ -110,78 +110,130 @@
|
|||||||
"value2" \
|
"value2" \
|
||||||
"value3"
|
"value3"
|
||||||
|
|
||||||
|
#define TEST_DATA_COMPLEX_SECTION_ITERATION_CONTEXT \
|
||||||
|
"{" \
|
||||||
|
" \"users\": [" \
|
||||||
|
" { \"name\": \"John\", \"age\": 30 }," \
|
||||||
|
" { \"name\": \"Jane\", \"age\": 25 }" \
|
||||||
|
" ]" \
|
||||||
|
"}"
|
||||||
|
|
||||||
|
#define TEST_DATA_COMPLEX_SECTION_ITERATION_TEMPLATE \
|
||||||
|
"{{#user users}}" \
|
||||||
|
" Name: {{user.name}}, Age: {{user.age}}\n" \
|
||||||
|
"{{/users}}"
|
||||||
|
|
||||||
|
#define TEST_DATA_COMPLEX_SECTION_ITERATION_RESULT \
|
||||||
|
" Name: John, Age: 30\n" \
|
||||||
|
" Name: Jane, Age: 25\n"
|
||||||
|
|
||||||
void test_eval_single_level_key(Arena *arena) {
|
void test_eval_single_level_key(Arena *arena) {
|
||||||
|
raise_notice("Testing single level key evaluation");
|
||||||
const char *context_text = arena_strdup(arena, "{\"name\": \"world\"}");
|
const char *context_text = arena_strdup(arena, "{\"name\": \"world\"}");
|
||||||
Json *context = json_parse(arena, &context_text);
|
Json *context = json_parse(arena, &context_text);
|
||||||
if (!context) { raise_exception("Malformed json"); exit(1); }
|
if (!context) { raise_exception("Malformed json"); exit(1); }
|
||||||
|
|
||||||
char *result = eval_string(arena, context, "name");
|
char *result = eval_string(arena, context, "name");
|
||||||
raise_debug("eval result: %s", result);
|
raise_notice("Context: %s", json_to_string(arena, context));
|
||||||
|
raise_notice("Query: name");
|
||||||
|
raise_notice("Result: %s", result);
|
||||||
assert(result && strcmp(result, "world") == 0);
|
assert(result && strcmp(result, "world") == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_eval_nested_key(Arena *arena) {
|
void test_eval_nested_key(Arena *arena) {
|
||||||
|
raise_notice("Testing nested key evaluation");
|
||||||
const char *context_text = arena_strdup(arena, "{\"person\": {\"name\": \"Alice\"}}");
|
const char *context_text = arena_strdup(arena, "{\"person\": {\"name\": \"Alice\"}}");
|
||||||
Json *context = json_parse(arena, &context_text);
|
Json *context = json_parse(arena, &context_text);
|
||||||
if (!context) { raise_exception("Malformed json"); exit(1); }
|
if (!context) { raise_exception("Malformed json"); exit(1); }
|
||||||
|
|
||||||
char *result = eval_string(arena, context, "person.name");
|
char *result = eval_string(arena, context, "person.name");
|
||||||
raise_notice("context: %s, eval result: %s", json_to_string(arena, context), result);
|
raise_notice("Context: %s", json_to_string(arena, context));
|
||||||
|
raise_notice("Query: person.name");
|
||||||
|
raise_notice("Result: %s", result);
|
||||||
assert(result && strcmp(result, "Alice") == 0);
|
assert(result && strcmp(result, "Alice") == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_render_interpolation_tags(Arena *arena) {
|
void test_render_interpolation_tags(Arena *arena) {
|
||||||
raise_trace("test_render_interpolation_tags(arena)");
|
raise_notice("Testing interpolation tags without prefix");
|
||||||
const char *context_text = arena_strdup(arena, TEST_DATA_INTERPOLATION_CONTEXT);
|
const char *context_text = arena_strdup(arena, TEST_DATA_INTERPOLATION_CONTEXT);
|
||||||
Json *context = json_parse(arena, &context_text);
|
Json *context = json_parse(arena, &context_text);
|
||||||
if (!context) { raise_exception("Malformed json"); exit(1); }
|
if (!context) { raise_exception("Malformed json"); exit(1); }
|
||||||
|
|
||||||
char *text = arena_strdup(arena, TEST_DATA_INTERPOLATION_TEMPLATE);
|
char *text = arena_strdup(arena, TEST_DATA_INTERPOLATION_TEMPLATE);
|
||||||
|
raise_notice("Template:\n%s", text);
|
||||||
|
raise_notice("Context: %s", json_to_string(arena, context));
|
||||||
|
|
||||||
hmpl_render_interpolation_tags(arena, &text, context, "");
|
hmpl_render_interpolation_tags(arena, &text, context, "");
|
||||||
raise_trace("text: %s", text);
|
raise_notice("Result:\n%s", text);
|
||||||
assert(strcmp(text, TEST_DATA_INTERPOLATION_RESULT) == 0);
|
assert(strcmp(text, TEST_DATA_INTERPOLATION_RESULT) == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_render_interpolation_tags_with_prefix(Arena *arena) {
|
void test_render_interpolation_tags_with_prefix(Arena *arena) {
|
||||||
|
raise_notice("Testing interpolation tags with prefix");
|
||||||
const char *context_text = arena_strdup(arena, TEST_DATA_INTERPOLATION_WITH_PREFIX_CONTEXT);
|
const char *context_text = arena_strdup(arena, TEST_DATA_INTERPOLATION_WITH_PREFIX_CONTEXT);
|
||||||
Json *context = json_parse(arena, &context_text);
|
Json *context = json_parse(arena, &context_text);
|
||||||
if (!context) { raise_exception("Malformed json"); exit(1); }
|
if (!context) { raise_exception("Malformed json"); exit(1); }
|
||||||
|
|
||||||
char *text = arena_strdup(arena, TEST_DATA_INTERPOLATION_WITH_PREFIX_TEMPLATE);
|
char *text = arena_strdup(arena, TEST_DATA_INTERPOLATION_WITH_PREFIX_TEMPLATE);
|
||||||
|
raise_notice("Template:\n%s", text);
|
||||||
|
raise_notice("Context: %s", json_to_string(arena, context));
|
||||||
|
|
||||||
hmpl_render_interpolation_tags(arena, &text, context, ".");
|
hmpl_render_interpolation_tags(arena, &text, context, ".");
|
||||||
|
raise_notice("Result:\n%s", text);
|
||||||
assert(strcmp(text, TEST_DATA_INTERPOLATION_WITH_PREFIX_RESULT) == 0);
|
assert(strcmp(text, TEST_DATA_INTERPOLATION_WITH_PREFIX_RESULT) == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_render_section_tags(Arena *arena) {
|
void test_render_section_tags(Arena *arena) {
|
||||||
|
raise_notice("Testing simple section tags");
|
||||||
const char *context_text = arena_strdup(arena, TEST_DATA_SIMPLE_SECTION_ITERATION_CONTEXT);
|
const char *context_text = arena_strdup(arena, TEST_DATA_SIMPLE_SECTION_ITERATION_CONTEXT);
|
||||||
Json *context = json_parse(arena, &context_text);
|
Json *context = json_parse(arena, &context_text);
|
||||||
if (!context) { raise_exception("Malformed json"); exit(1); }
|
if (!context) { raise_exception("Malformed json"); exit(1); }
|
||||||
|
|
||||||
char *text = arena_strdup(arena, TEST_DATA_SIMPLE_SECTION_ITERATION_TEMPLATE);
|
char *text = arena_strdup(arena, TEST_DATA_SIMPLE_SECTION_ITERATION_TEMPLATE);
|
||||||
|
raise_notice("Template:\n%s", text);
|
||||||
|
raise_notice("Context: %s", json_to_string(arena, context));
|
||||||
|
|
||||||
hmpl_render_section_tags(arena, &text, context, "#", "/", " ");
|
hmpl_render_section_tags(arena, &text, context, "#", "/", " ");
|
||||||
|
raise_notice("Result:\n%s", text);
|
||||||
assert(strcmp(text, TEST_DATA_SIMPLE_SECTION_ITERATION_RESULT) == 0);
|
assert(strcmp(text, TEST_DATA_SIMPLE_SECTION_ITERATION_RESULT) == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void test_render_complex_section_tags(Arena *arena) {
|
||||||
|
raise_notice("Testing complex section tags");
|
||||||
|
const char *context_text = arena_strdup(arena, TEST_DATA_COMPLEX_SECTION_ITERATION_CONTEXT);
|
||||||
|
Json *context = json_parse(arena, &context_text);
|
||||||
|
if (!context) { raise_exception("Malformed json"); exit(1); }
|
||||||
|
|
||||||
|
char *text = arena_strdup(arena, TEST_DATA_COMPLEX_SECTION_ITERATION_TEMPLATE);
|
||||||
|
raise_notice("Template:\n%s", text);
|
||||||
|
raise_notice("Context: %s", json_to_string(arena, context));
|
||||||
|
|
||||||
|
hmpl_render_section_tags(arena, &text, context, "#", "/", " ");
|
||||||
|
raise_notice("Result:\n%s", text);
|
||||||
|
assert(strcmp(text, TEST_DATA_COMPLEX_SECTION_ITERATION_RESULT) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
init_logger();
|
init_logger();
|
||||||
|
raise_notice("Starting HMPL tests");
|
||||||
Arena arena = arena_init(MEM_MiB * 3);
|
Arena arena = arena_init(MEM_MiB * 3);
|
||||||
|
|
||||||
// evaluation
|
// evaluation
|
||||||
|
raise_notice("=== Testing key evaluation ===");
|
||||||
test_eval_single_level_key(&arena);
|
test_eval_single_level_key(&arena);
|
||||||
test_eval_nested_key(&arena);
|
test_eval_nested_key(&arena);
|
||||||
|
|
||||||
// interpolation tags
|
// interpolation tags
|
||||||
|
raise_notice("=== Testing interpolation tags ===");
|
||||||
test_render_interpolation_tags(&arena);
|
test_render_interpolation_tags(&arena);
|
||||||
test_render_interpolation_tags_with_prefix(&arena);
|
test_render_interpolation_tags_with_prefix(&arena);
|
||||||
|
|
||||||
// section tags
|
// section tags
|
||||||
|
raise_notice("=== Testing section tags ===");
|
||||||
test_render_section_tags(&arena);
|
test_render_section_tags(&arena);
|
||||||
|
test_render_complex_section_tags(&arena);
|
||||||
|
|
||||||
printf("All tests passed.\n");
|
raise_notice("All tests passed successfully");
|
||||||
|
|
||||||
arena_free(&arena);
|
arena_free(&arena);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{ stdenv, gcc, lib, hectic, bash }:
|
{ stdenv, gcc, lib, bash, gdb }:
|
||||||
|
|
||||||
stdenv.mkDerivation {
|
stdenv.mkDerivation {
|
||||||
pname = "watch";
|
pname = "watch";
|
||||||
version = "1.0";
|
version = "1.0";
|
||||||
src = ./.;
|
src = ./.;
|
||||||
doCheck = true;
|
doCheck = false;
|
||||||
|
|
||||||
nativeBuildInputs = [ gcc gdb ];
|
nativeBuildInputs = [ gcc gdb ];
|
||||||
|
|
||||||
@@ -17,10 +17,8 @@ stdenv.mkDerivation {
|
|||||||
'';
|
'';
|
||||||
|
|
||||||
installPhase = ''
|
installPhase = ''
|
||||||
mkdir -p $out/bin $out/lib $out/include
|
mkdir -p $out/bin
|
||||||
cp target/hmpl $out/bin/hmpl
|
cp target/watch $out/bin/watch
|
||||||
cp target/libhmpl.a $out/lib/
|
|
||||||
cp hmpl.h $out/include/hmpl.h
|
|
||||||
'';
|
'';
|
||||||
|
|
||||||
meta = {
|
meta = {
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
#include <fnmatch.h>
|
#include <fnmatch.h>
|
||||||
#include <getopt.h>
|
#include <getopt.h>
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <signal.h>
|
||||||
|
|
||||||
#ifndef PATH_MAX
|
#ifndef PATH_MAX
|
||||||
#define PATH_MAX 4096
|
#define PATH_MAX 4096
|
||||||
@@ -19,15 +21,27 @@
|
|||||||
#define MAX_PATTERNS 32
|
#define MAX_PATTERNS 32
|
||||||
#define POLL_INTERVAL_MS 100
|
#define POLL_INTERVAL_MS 100
|
||||||
|
|
||||||
|
// Global flag to indicate if we're running in pager mode
|
||||||
|
int pager_mode = 0;
|
||||||
|
FILE *output_stream = NULL;
|
||||||
|
int running = 1;
|
||||||
|
|
||||||
|
void signal_handler(int sig) {
|
||||||
|
(void)sig; // Mark parameter as used to avoid warning
|
||||||
|
running = 0;
|
||||||
|
}
|
||||||
|
|
||||||
void print_usage(const char *prog_name) {
|
void print_usage(const char *prog_name) {
|
||||||
fprintf(stderr, "Usage: %s <command> [-p <pattern1>] [-p <pattern2>] ... <dir1> [dir2] ...\n", prog_name);
|
fprintf(stderr, "Usage: %s <command> [-p <pattern1>] [-p <pattern2>] ... <dir1> [dir2] ...\n", prog_name);
|
||||||
fprintf(stderr, " or: find . -type d | %s <command> [-p <pattern1>] [-p <pattern2>] ...\n", prog_name);
|
fprintf(stderr, " or: find . -type d | %s <command> [-p <pattern1>] [-p <pattern2>] ...\n", prog_name);
|
||||||
fprintf(stderr, "Options:\n");
|
fprintf(stderr, "Options:\n");
|
||||||
fprintf(stderr, " -p <pattern> File pattern to watch (can be used multiple times)\n");
|
fprintf(stderr, " -p <pattern> File pattern to watch (can be used multiple times)\n");
|
||||||
|
fprintf(stderr, " -P Enable pager-friendly output (refresh mode)\n");
|
||||||
fprintf(stderr, " -h Show this help message\n");
|
fprintf(stderr, " -h Show this help message\n");
|
||||||
fprintf(stderr, "Examples:\n");
|
fprintf(stderr, "Examples:\n");
|
||||||
fprintf(stderr, " %s 'make' -p '*.c' -p '*.h' ./src\n", prog_name);
|
fprintf(stderr, " %s 'make' -p '*.c' -p '*.h' ./src\n", prog_name);
|
||||||
fprintf(stderr, " find . -type d | %s 'echo changed' -p '*.py'\n", prog_name);
|
fprintf(stderr, " find . -type d | %s 'echo changed' -p '*.py'\n", prog_name);
|
||||||
|
fprintf(stderr, " %s -P 'make' -p '*.c' -p '*.h' ./src | less\n", prog_name);
|
||||||
exit(EXIT_FAILURE);
|
exit(EXIT_FAILURE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +162,10 @@ int match_any_pattern(const char *filename, char **patterns, int num_patterns) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char *argv[]) {
|
||||||
|
// Register signal handlers
|
||||||
|
signal(SIGINT, signal_handler);
|
||||||
|
signal(SIGTERM, signal_handler);
|
||||||
|
|
||||||
if (argc < 2) {
|
if (argc < 2) {
|
||||||
print_usage(argv[0]);
|
print_usage(argv[0]);
|
||||||
}
|
}
|
||||||
@@ -159,7 +177,7 @@ int main(int argc, char *argv[]) {
|
|||||||
optind = 2;
|
optind = 2;
|
||||||
int opt;
|
int opt;
|
||||||
|
|
||||||
while ((opt = getopt(argc, argv, "p:h")) != -1) {
|
while ((opt = getopt(argc, argv, "p:Ph")) != -1) {
|
||||||
switch (opt) {
|
switch (opt) {
|
||||||
case 'p':
|
case 'p':
|
||||||
if (num_patterns < MAX_PATTERNS) {
|
if (num_patterns < MAX_PATTERNS) {
|
||||||
@@ -169,6 +187,9 @@ int main(int argc, char *argv[]) {
|
|||||||
exit(EXIT_FAILURE);
|
exit(EXIT_FAILURE);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'P':
|
||||||
|
pager_mode = 1;
|
||||||
|
break;
|
||||||
case 'h':
|
case 'h':
|
||||||
print_usage(argv[0]);
|
print_usage(argv[0]);
|
||||||
break;
|
break;
|
||||||
@@ -183,6 +204,15 @@ int main(int argc, char *argv[]) {
|
|||||||
print_usage(argv[0]);
|
print_usage(argv[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set output stream - stderr for normal mode, stdout for pager mode
|
||||||
|
output_stream = pager_mode ? stdout : stderr;
|
||||||
|
|
||||||
|
// Check if stdout is being piped and we're not in pager mode
|
||||||
|
if (!pager_mode && !isatty(STDOUT_FILENO)) {
|
||||||
|
pager_mode = 1;
|
||||||
|
output_stream = stdout;
|
||||||
|
}
|
||||||
|
|
||||||
struct dir_info dirs[MAX_DIRS];
|
struct dir_info dirs[MAX_DIRS];
|
||||||
int num_dirs = 0;
|
int num_dirs = 0;
|
||||||
struct file_hash files;
|
struct file_hash files;
|
||||||
@@ -242,13 +272,13 @@ int main(int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Print the directories and patterns we're watching
|
// Print the directories and patterns we're watching
|
||||||
fprintf(stderr, "Watching %d directories for files matching: ", num_dirs);
|
fprintf(output_stream, "Watching %d directories for files matching: ", num_dirs);
|
||||||
for (int i = 0; i < num_patterns; i++) {
|
for (int i = 0; i < num_patterns; i++) {
|
||||||
fprintf(stderr, "'%s'%s", patterns[i], (i < num_patterns - 1) ? ", " : "\n");
|
fprintf(output_stream, "'%s'%s", patterns[i], (i < num_patterns - 1) ? ", " : "\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < num_dirs; i++) {
|
for (int i = 0; i < num_dirs; i++) {
|
||||||
fprintf(stderr, " %s\n", dirs[i].path);
|
fprintf(output_stream, " %s\n", dirs[i].path);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < num_dirs; i++) {
|
for (int i = 0; i < num_dirs; i++) {
|
||||||
@@ -281,8 +311,9 @@ int main(int argc, char *argv[]) {
|
|||||||
closedir(dir);
|
closedir(dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
fprintf(stderr, "Initially found %d matching files\n", files.count);
|
fprintf(output_stream, "Initially found %d matching files\n", files.count);
|
||||||
fprintf(stderr, "Waiting for file modifications...\n");
|
fprintf(output_stream, "Waiting for file modifications...\n");
|
||||||
|
fflush(output_stream);
|
||||||
|
|
||||||
struct timeval tv;
|
struct timeval tv;
|
||||||
tv.tv_sec = 0;
|
tv.tv_sec = 0;
|
||||||
@@ -302,7 +333,8 @@ int main(int argc, char *argv[]) {
|
|||||||
|
|
||||||
int first_scan = 1;
|
int first_scan = 1;
|
||||||
|
|
||||||
while (1) {
|
// Main watch loop
|
||||||
|
while (running) {
|
||||||
select(0, NULL, NULL, NULL, &tv);
|
select(0, NULL, NULL, NULL, &tv);
|
||||||
|
|
||||||
int any_changes = 0;
|
int any_changes = 0;
|
||||||
@@ -337,14 +369,14 @@ int main(int argc, char *argv[]) {
|
|||||||
existing->exists = 1;
|
existing->exists = 1;
|
||||||
if (existing->mtime != st.st_mtime) {
|
if (existing->mtime != st.st_mtime) {
|
||||||
if (!first_scan) {
|
if (!first_scan) {
|
||||||
fprintf(stderr, "File modified: %s\n", filepath);
|
fprintf(output_stream, "File modified: %s\n", filepath);
|
||||||
any_changes = 1;
|
any_changes = 1;
|
||||||
}
|
}
|
||||||
existing->mtime = st.st_mtime;
|
existing->mtime = st.st_mtime;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!first_scan) {
|
if (!first_scan) {
|
||||||
fprintf(stderr, "New file: %s\n", filepath);
|
fprintf(output_stream, "New file: %s\n", filepath);
|
||||||
any_changes = 1;
|
any_changes = 1;
|
||||||
}
|
}
|
||||||
struct file_info *file = malloc(sizeof(struct file_info));
|
struct file_info *file = malloc(sizeof(struct file_info));
|
||||||
@@ -361,7 +393,7 @@ int main(int argc, char *argv[]) {
|
|||||||
for (int i = 0; i < files.size; i++) {
|
for (int i = 0; i < files.size; i++) {
|
||||||
if (files.items[i] != NULL && files.items[i]->exists == 0) {
|
if (files.items[i] != NULL && files.items[i]->exists == 0) {
|
||||||
if (!first_scan) {
|
if (!first_scan) {
|
||||||
fprintf(stderr, "File deleted: %s\n", files.items[i]->path);
|
fprintf(output_stream, "File deleted: %s\n", files.items[i]->path);
|
||||||
any_changes = 1;
|
any_changes = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,8 +401,42 @@ int main(int argc, char *argv[]) {
|
|||||||
|
|
||||||
hash_remove_nonexistent(&files);
|
hash_remove_nonexistent(&files);
|
||||||
|
|
||||||
|
// If in pager mode and any changes occur, clear screen and reprint status
|
||||||
|
if (pager_mode && any_changes) {
|
||||||
|
// Use ANSI escape codes to clear screen
|
||||||
|
fprintf(output_stream, "\033[2J\033[H");
|
||||||
|
|
||||||
|
// Reprint header
|
||||||
|
fprintf(output_stream, "Watching %d directories for files matching: ", num_dirs);
|
||||||
|
for (int i = 0; i < num_patterns; i++) {
|
||||||
|
fprintf(output_stream, "'%s'%s", patterns[i], (i < num_patterns - 1) ? ", " : "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// List current files
|
||||||
|
fprintf(output_stream, "Currently watching %d files:\n", files.count);
|
||||||
|
int file_count = 0;
|
||||||
|
for (int i = 0; i < files.size && file_count < files.count; i++) {
|
||||||
|
if (files.items[i] != NULL) {
|
||||||
|
fprintf(output_stream, " %s\n", files.items[i]->path);
|
||||||
|
file_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show last action
|
||||||
|
time_t current_time = time(NULL);
|
||||||
|
fprintf(output_stream, "\nLast event: Command executed at %s", ctime(¤t_time));
|
||||||
|
fprintf(output_stream, "Command: %s\n", command);
|
||||||
|
|
||||||
|
// Make sure output is flushed to the pager
|
||||||
|
fflush(output_stream);
|
||||||
|
} else if (any_changes) {
|
||||||
|
// In normal mode, just show status messages
|
||||||
|
fprintf(output_stream, "Executing command: %s\n", command);
|
||||||
|
fflush(output_stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute command if changes detected
|
||||||
if (any_changes) {
|
if (any_changes) {
|
||||||
fprintf(stderr, "Executing command: %s\n", command);
|
|
||||||
int res = system(command);
|
int res = system(command);
|
||||||
if (res != 0) {
|
if (res != 0) {
|
||||||
perror("system");
|
perror("system");
|
||||||
@@ -393,5 +459,6 @@ int main(int argc, char *argv[]) {
|
|||||||
free(patterns);
|
free(patterns);
|
||||||
hash_free(&files);
|
hash_free(&files);
|
||||||
|
|
||||||
|
fprintf(output_stream, "Watch terminated.\n");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
pkgs.writeShellScriptBin "pager" ''
|
pkgs.writeShellScriptBin "pager" ''
|
||||||
nvim -R --clean -c 'set buftype=nofile' -c 'nnoremap q :q!<CR>' -c 'set nowrap' \
|
nvim -R --clean -c 'set buftype=nofile' -c 'nnoremap q :q!<CR>' -c 'set nowrap' \
|
||||||
-c 'set runtimepath^=${pkgs.vimPlugins.vim-plugin-AnsiEsc}' \
|
-c 'set runtimepath^=${pkgs.vimPlugins.vim-plugin-AnsiEsc}' \
|
||||||
-c 'runtime! plugin/*.vim' -c 'AnsiEsc' -
|
-c 'runtime! plugin/*.vim' -c 'AnsiEsc' - $@
|
||||||
# ^^^^^^^^^^^^^^^^^^^^
|
# ^^^^^^^^^^^^^^^^^^^^
|
||||||
# Prevents Neovim from treating the buffer as a file
|
# Prevents Neovim from treating the buffer as a file
|
||||||
# ^^^^^^^^^^^^^^^^^^^^
|
# ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|||||||
Reference in New Issue
Block a user