feat: static page for fun

This commit is contained in:
2025-04-23 11:09:09 +00:00
parent cc7de6c0dd
commit cda46a7e13
10 changed files with 26 additions and 24 deletions

2
package/c/hemar/.gitignore vendored Executable file
View File

@@ -0,0 +1,2 @@
hemar.o
hemar.so

15
package/c/hemar/Makefile Normal file
View File

@@ -0,0 +1,15 @@
MODULE_big = hemar
OBJS = hemar.o
EXTENSION = hemar
DATA = $(wildcard *.sql)
HECTIC_CONFIG = hectic-config
PG_CONFIG = pg_config
PG_CFLAGS += $(shell $(HECTIC_CONFIG) --cflags)
PG_LDFLAGS += -Wl,-rpath,$(shell $(HECTIC_CONFIG) --libdir)
SHLIB_LINK += $(shell $(HECTIC_CONFIG) --libs)
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)

26
package/c/hemar/hemar--0.1.sql Executable file
View File

@@ -0,0 +1,26 @@
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
\echo Use "CREATE EXTENSION hemar" to load this file. \quit
CREATE SCHEMA hemar;
-- Define the parse_text_with_hectic function that uses hectic library
-- Expected usage:
-- ```sql
-- SELECT "hemar"."render"(
-- "declare" :=
-- jsonb_build_object(
-- 'name', 'test',
-- 'config', jsonb_build_object(
-- 'debug', true,
-- 'limit', 100
-- )
-- ),
-- "template" := $hemar$
-- {{ name }} {{ config.limit }}
-- $hemar$
-- );
-- ```
CREATE FUNCTION "hemar"."render"("declare" json, "template" text)
RETURNS text
AS 'hemar', 'render'
LANGUAGE C STRICT;

363
package/c/hemar/hemar.c Executable file
View File

@@ -0,0 +1,363 @@
#include <postgres.h>
#include <fmgr.h>
#include <utils/builtins.h>
#include <utils/json.h>
#include "hectic.h"
#include <string.h>
#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif
/* helper function to get a JSON value by key path */
static Json *json_get_by_path(Arena *arena, const Json *context, const char *key_path) {
char *path_copy;
char *token;
Json *current;
if (!context || !key_path || !*key_path) {
return NULL;
}
path_copy = arena_strdup(arena, key_path);
token = strtok(path_copy, ".");
current = (Json*)context;
while (token && current) {
current = json_get_object_item(current, token);
token = strtok(NULL, ".");
}
return current;
}
/* Convert JSON value to string */
static char *json_value_to_string(Arena *arena, const Json *json) {
if (!json) {
return "";
}
switch (json->type) {
case JSON_STRING:
return json->value.string;
case JSON_NUMBER: {
char *buf = arena_alloc(arena, 64);
snprintf(buf, 64, "%.6g", json->value.number);
return buf;
}
case JSON_BOOL:
return json->value.boolean ? "true" : "false";
case JSON_NULL:
return "";
case JSON_ARRAY:
case JSON_OBJECT:
return json_to_string(arena, json);
default:
return "";
}
}
/* Forward declaration for recursive function */
static char *render_template_node(Arena *arena, const TemplateNode *node, const Json *context);
/* Render a text node */
static char *render_text_node(Arena *arena, const TemplateNode *node) {
if (!node || node->type != TEMPLATE_NODE_TEXT) {
return "";
}
return node->value.text.content;
}
/* Render an interpolation node */
static char *render_interpolation_node(Arena *arena, const TemplateNode *node, const Json *context) {
const char *key;
Json *value;
if (!node || node->type != TEMPLATE_NODE_INTERPOLATE || !context) {
return "";
}
key = node->value.interpolate.key;
value = json_get_by_path(arena, context, key);
if (!value) {
return "";
}
return json_value_to_string(arena, value);
}
/* Render a section node (for loop) */
static char *render_section_node(Arena *arena, const TemplateNode *node, const Json *context) {
const char *collection_key;
const char *iterator_name;
TemplateNode *body;
Json *collection;
size_t buffer_size;
char *buffer;
size_t buffer_pos;
Json *item;
const char *empty_json;
Json *iter_context;
Json *item_json;
char *rendered_body;
size_t rendered_len;
if (!node || node->type != TEMPLATE_NODE_SECTION || !context) {
return "";
}
collection_key = node->value.section.collection;
iterator_name = node->value.section.iterator;
body = node->value.section.body;
collection = json_get_by_path(arena, context, collection_key);
if (!collection || collection->type != JSON_ARRAY) {
return "";
}
buffer_size = 1024;
buffer = arena_alloc(arena, buffer_size);
buffer_pos = 0;
item = collection->value.child;
while (item) {
empty_json = "{}";
iter_context = json_parse(arena, &empty_json);
if (!iter_context) {
return "";
}
item_json = arena_alloc(arena, sizeof(Json));
memcpy(item_json, item, sizeof(Json));
item_json->key = arena_strdup(arena, iterator_name);
item_json->next = NULL;
rendered_body = render_template_node(arena, body, iter_context);
rendered_len = strlen(rendered_body);
if (buffer_pos + rendered_len + 1 > buffer_size) {
buffer_size = (buffer_pos + rendered_len + 1) * 2;
buffer = arena_realloc(arena, buffer, buffer_size / 2, buffer_size);
}
strcpy(buffer + buffer_pos, rendered_body);
buffer_pos += rendered_len;
item = item->next;
}
buffer[buffer_pos] = '\0';
return buffer;
}
/* Render an include node */
static char *render_include_node(Arena *arena, const TemplateNode *node, const Json *context) {
const char *include_key;
Json *include_value;
char *buffer;
size_t buffer_pos;
Json *include_item;
Json *template_json;
Json *content_json;
Json *context_json;
if (!node || node->type != TEMPLATE_NODE_INCLUDE || !context) {
return "";
}
include_key = node->value.include.key;
include_value = json_get_by_path(arena, context, include_key);
if (!include_value || include_value->type != JSON_ARRAY) {
return "";
}
buffer = arena_alloc(arena, 1024);
buffer_pos = 0;
include_item = include_value->value.child;
while (include_item) {
if (include_item->type == JSON_OBJECT) {
template_json = json_get_object_item(include_item, "template");
content_json = json_get_object_item(include_item, "content");
context_json = json_get_object_item(include_item, "context");
if (template_json && template_json->type == JSON_STRING) {
const char *template_str = template_json->value.string;
const Json *include_context = context_json ? context_json : context;
TemplateConfig config = template_default_config();
TemplateResult template_result = template_parse(arena, &template_str, &config);
if (!IS_RESULT_ERROR(template_result)) {
TemplateNode template_node = RESULT_SOME_VALUE(template_result);
char *rendered = render_template_node(arena, &template_node, include_context);
buffer_pos += sprintf(buffer + buffer_pos, "%s", rendered);
}
} else if (content_json && content_json->type == JSON_STRING) {
buffer_pos += sprintf(buffer + buffer_pos, "%s", content_json->value.string);
}
}
include_item = include_item->next;
}
buffer[buffer_pos] = '\0';
return buffer;
}
/* Render a template node tree recursively */
static char *render_template_node(Arena *arena, const TemplateNode *node, const Json *context) {
size_t buffer_size = 4096;
char *output = arena_alloc(arena, buffer_size);
size_t output_pos = 0;
size_t rendered_len;
const TemplateNode *current;
char *rendered;
if (!node) {
return "";
}
current = node;
while (current) {
rendered = NULL;
switch (current->type) {
case TEMPLATE_NODE_TEXT:
rendered = render_text_node(arena, current);
break;
case TEMPLATE_NODE_INTERPOLATE:
rendered = render_interpolation_node(arena, current, context);
break;
case TEMPLATE_NODE_SECTION:
rendered = render_section_node(arena, current, context);
break;
case TEMPLATE_NODE_INCLUDE:
rendered = render_include_node(arena, current, context);
break;
case TEMPLATE_NODE_EXECUTE:
todo;
rendered = "";
break;
default:
rendered = "";
break;
}
rendered_len = strlen(rendered);
if (output_pos + rendered_len + 1 > buffer_size) {
buffer_size = (output_pos + rendered_len + 1) * 2;
output = arena_realloc(arena, output, buffer_size / 2, buffer_size);
}
strcpy(output + output_pos, rendered);
output_pos += rendered_len;
if (current->children) {
char *children_rendered = render_template_node(arena, current->children, context);
size_t children_len = strlen(children_rendered);
if (output_pos + children_len + 1 > buffer_size) {
buffer_size = (output_pos + children_len + 1) * 2;
output = arena_realloc(arena, output, buffer_size / 2, buffer_size);
}
strcpy(output + output_pos, children_rendered);
output_pos += children_len;
}
current = current->next;
}
output[output_pos] = '\0';
return output;
}
/* Define the function render */
PG_FUNCTION_INFO_V1(render);
/*
* Function to render templates using hectic library with JSON context
* Arguments:
* 1. declare - JSON context for rendering
* 2. template - The template text to render
*/
Datum render(PG_FUNCTION_ARGS)
{
text *context_text = PG_GETARG_TEXT_PP(0);
text *template_text = PG_GETARG_TEXT_PP(1);
/* Convert input text to C string */
char *template_str = text_to_cstring(template_text);
char *context_str = text_to_cstring(context_text);
/* Initialize arena for memory management */
Arena arena = arena_init(MEM_MiB);
TemplateNode root_node;
TemplateResult template_result;
TemplateConfig config;
Json *context;
const char *template_ptr;
char *result_str;
text *result;
/* Parse the JSON context */
const char *json_ptr = context_str;
context = json_parse(&arena, &json_ptr);
if (!context) {
arena_free(&arena);
ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("Invalid JSON context")));
}
/* Parse the template text */
template_ptr = template_str;
config = template_default_config();
template_result = template_parse(&arena, &template_ptr, &config);
if (IS_RESULT_ERROR(template_result)) {
arena_free(&arena);
ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("Failed to parse template: %s",
RESULT_ERROR_MESSAGE(template_result))));
}
/* Render the template */
root_node = RESULT_SOME_VALUE(template_result);
result_str = render_template_node(&arena, &root_node, context);
/* Prepare return value */
result = cstring_to_text(result_str);
arena_free(&arena);
PG_RETURN_TEXT_P(result);
}

3
package/c/hemar/hemar.control Executable file
View File

@@ -0,0 +1,3 @@
comment = 'My first extension'
default_version = '0.1'
module_pathname = '$libdir/hemar'

108
package/c/hemar/make.sh Executable file
View File

@@ -0,0 +1,108 @@
#!/bin/sh
# Usage: make.sh [build|watch] [--debug] [--color]
# Options:
# build Build the postgres extension (default if no mode is provided).
# watch Build the extension and watch for changes.
# --debug Build with -O0 (debug mode).
# --color Pass -fdiagnostics-color=always to compiler.
# help, --help Show this help message.
check_dependencies() {
for dep in gcc pg_config; do
if ! command -v "$dep" >/dev/null 2>&1; then
echo "Error: Required dependency '$dep' not found." >&2
exit 1
fi
done
# Check for either fswatch or inotifywait for watch mode
if [ "$MODE" = "watch" ] && ! command -v fswatch >/dev/null 2>&1 && ! command -v inotifywait >/dev/null 2>&1; then
echo "Error: Neither fswatch nor inotifywait found. Please install one of them." >&2
echo " On macOS: brew install fswatch" >&2
echo " On Linux: sudo apt install inotify-tools" >&2
exit 1
fi
}
print_help() {
cat <<EOF
Usage: $0 [build|watch] [--debug] [--color]
build Build the postgres extension (default).
watch Build the extension and watch for changes.
--debug Build with debug flags (-O0).
--color Force colored compiler diagnostics.
help, --help Display this help message.
EOF
}
# Show help if requested
case "$1" in
help|--help)
print_help
exit 0
;;
esac
# Default flags
OPTFLAGS="-O2"
CFLAGS="-Wall -Wextra -pedantic -fPIC"
COLOR_FLAG=""
DEBUG=0
# Process options
while [ $# -gt 0 ]; do
case "$1" in
--debug)
OPTFLAGS="-O0 -gdwarf-2 -g3 -Wno-error"
DEBUG=1
;;
--color)
COLOR_FLAG="-fdiagnostics-color=always"
;;
*)
break
;;
esac
shift
done
MODE="${1:-build}"
shift 2> /dev/null
if [ -n "$COLOR_FLAG" ]; then
CFLAGS="$CFLAGS $COLOR_FLAG"
fi
check_dependencies
# Get PostgreSQL include directory
PG_INCLUDE=$(pg_config --includedir-server)
PG_LIBDIR=$(pg_config --libdir)
case "$MODE" in
watch)
find . -type d | nix run .#watch -- 'sh ./make.sh build' -p '*.c' -p '*.h' 2>&1
;;
build)
mkdir -p target
echo "# Building PostgreSQL extension"
# Get hectic library paths from nix
HECTIC_PATH=$(nix build --print-out-paths -f ../../../. c-hectic)
HECTIC_INCLUDE="$HECTIC_PATH/include"
HECTIC_LIB="$HECTIC_PATH/lib"
# shellcheck disable=SC2086
gcc $CFLAGS $OPTFLAGS -I$PG_INCLUDE -I$HECTIC_INCLUDE -shared -o target/hemar.so hemar.c -L$HECTIC_LIB -lhectic
# Copy extension files to target directory
cp hemar.control target/
cp hemar--0.1.sql target/
echo "Build complete. Files available in target/ directory."
;;
*)
print_help
exit 1
;;
esac

0
package/c/hemar/test/01-test.c Executable file
View File