Skip to content

Commit

Permalink
C API for streaming content mutations
Browse files Browse the repository at this point in the history
  • Loading branch information
kornelski committed Nov 6, 2024
1 parent 5c1fc4a commit 212155d
Show file tree
Hide file tree
Showing 12 changed files with 626 additions and 220 deletions.
6 changes: 5 additions & 1 deletion c-api/c-tests/src/test.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,9 @@ int run_tests() {
subtest("Element API", element_api_test);
subtest("Document end API", document_end_api_test);
subtest("Memory limiting", test_memory_limiting);
return done_testing();
int res = done_testing();
if (res) {
fprintf(stderr, "\nSome tests have failed\n");
}
return res;
}
109 changes: 109 additions & 0 deletions c-api/c-tests/src/test_element_api.c
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,114 @@ static void test_insert_content_around_element(lol_html_selector_t *selector, vo
);
}

//-------------------------------------------------------------------------
EXPECT_OUTPUT(
streaming_mutations_output_sink,
"&amp;before<div><!--prepend-->Hi<!--append--></div>&amp;after\xf0\x9f\x98\x82",
&EXPECTED_USER_DATA,
sizeof(EXPECTED_USER_DATA)
);

static void loltest_drop(void *user_data) {
int *drops = user_data;
(*drops)++;
}

static int loltest_write_all_callback_before(lol_html_streaming_sink_t *sink, void *user_data) {
int *counter = user_data;
ok(*counter >= 100 && *counter <= 103);

const char *before = "&before";
return lol_html_streaming_sink_write_str(sink, before, strlen(before), false);
}

static int loltest_write_all_callback_after(lol_html_streaming_sink_t *sink, void *user_data) {
int *counter = user_data;
ok(*counter >= 100 && *counter <= 103);

const char *after = "&after";
const char emoji[] = {0xf0,0x9f,0x98,0x82};
return lol_html_streaming_sink_write_str(sink, after, strlen(after), false) ||
lol_html_streaming_sink_write_str(sink, emoji, 4, false);
}

static int loltest_write_all_callback_prepend(lol_html_streaming_sink_t *sink, void *user_data) {
int *counter = user_data;
ok(*counter >= 100 && *counter <= 103);

const char *prepend1 = "<!--pre";
const char *prepend2 = "pend-->";
return lol_html_streaming_sink_write_str(sink, prepend1, strlen(prepend1), true) ||
lol_html_streaming_sink_write_str(sink, prepend2, strlen(prepend2), true);
}

static int loltest_write_all_callback_append(lol_html_streaming_sink_t *sink, void *user_data) {
int *counter = user_data;
ok(*counter >= 100 && *counter <= 103);

const char *append = "<!--append-->";
return lol_html_streaming_sink_write_str(sink, append, strlen(append), true);
}

static lol_html_rewriter_directive_t streaming_mutations_around_element(
lol_html_element_t *element,
void *user_data
) {
note("Stream before/prepend");
ok(!lol_html_element_streaming_before(element, &(lol_html_streaming_handler_t){
.write_all_callback = loltest_write_all_callback_before,
.user_data = user_data,
.drop_callback = loltest_drop,
}));
ok(!lol_html_element_streaming_prepend(element, &(lol_html_streaming_handler_t){
.write_all_callback = loltest_write_all_callback_prepend,
.user_data = user_data,
// tests null drop callback
}));
note("Stream after/append");
ok(!lol_html_element_streaming_append(element, &(lol_html_streaming_handler_t){
.write_all_callback = loltest_write_all_callback_append,
.user_data = user_data,
.drop_callback = loltest_drop,
}));
ok(!lol_html_element_streaming_after(element, &(lol_html_streaming_handler_t){
.write_all_callback = loltest_write_all_callback_after,
.user_data = user_data,
.drop_callback = loltest_drop,
}));

return LOL_HTML_CONTINUE;
}

static void test_streaming_mutations_around_element(lol_html_selector_t *selector, void *user_data) {
UNUSED(user_data);
lol_html_rewriter_builder_t *builder = lol_html_rewriter_builder_new();

int drop_count = 100;

int err = lol_html_rewriter_builder_add_element_content_handlers(
builder,
selector,
&streaming_mutations_around_element,
&drop_count,
NULL,
NULL,
NULL,
NULL
);

ok(!err);

run_rewriter(
builder,
"<div>Hi</div>",
streaming_mutations_output_sink,
user_data
);

ok(drop_count == 103); // one has no drop callback on purpose
}

//-------------------------------------------------------------------------
EXPECT_OUTPUT(
set_element_inner_content_output_sink,
Expand Down Expand Up @@ -706,6 +814,7 @@ void element_api_test() {
test_iterate_attributes(selector, &user_data);
test_get_and_modify_attributes(selector, &user_data);
test_insert_content_around_element(selector, &user_data);
test_streaming_mutations_around_element(selector, &user_data);

lol_html_selector_free(selector);
}
Expand Down
16 changes: 16 additions & 0 deletions c-api/cbindgen.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# To generate a header:
#
# cargo expand > tmp.rs
# cbindgen tmp.rs

language = "C"
tab_width = 4
documentation = true
documentation_style = "c99"
documentation_length = "full"

[export]
prefix = "lol_html_"

[export.mangle]
rename_types = "SnakeCase"
228 changes: 228 additions & 0 deletions c-api/include/lol_html.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ typedef struct lol_html_Element lol_html_element_t;
typedef struct lol_html_AttributesIterator lol_html_attributes_iterator_t;
typedef struct lol_html_Attribute lol_html_attribute_t;
typedef struct lol_html_Selector lol_html_selector_t;
typedef struct lol_html_CStreamingHandlerSink lol_html_streaming_sink_t;

// Library-allocated UTF8 string fat pointer.
//
Expand Down Expand Up @@ -116,6 +117,30 @@ typedef lol_html_rewriter_directive_t (*lol_html_end_tag_handler_t)(
void *user_data
);

// For use with streaming content handlers.
//
// Safety: the user data and the callbacks must be safe to use from a different thread (e.g. can't rely on thread-local storage).
// It doesn't have to be `Sync`, it will be used only by one thread at a time.
//
// Handler functions copy this struct. It can (and should) be created on the stack.
typedef struct lol_html_CStreamingHandler {
// Anything you like
void *user_data;
// Called when the handler is supposed to produce its output. Return `0` for success.
// The `sink` argument is guaranteed non-`NULL`. It is valid only for the duration of this call, and can only be used on the same thread.
// The sink is for [`lol_html_streaming_sink_write_str`].
// `user_data` comes from this struct.
//
// `write_all_callback` must not be `NULL`.
int (*write_all_callback)(lol_html_streaming_sink_t *sink, void *user_data);
// Called exactly once, after the last use of this handler.
// It may be `NULL`.
// `user_data` comes from this struct.
void (*drop_callback)(void *user_data);
// *Always* initialize to `NULL`.
void *reserved;
} lol_html_streaming_handler_t;

// Selector
//---------------------------------------------------------------------

Expand Down Expand Up @@ -792,6 +817,209 @@ int lol_html_doc_end_append(
bool is_html
);



//[`Element::streaming_prepend`]
//
// The [`CStreamingHandler`] contains callbacks that will be called
// when the content needs to be written.
//
// `streaming_writer` is copied immediately, and doesn't have a stable address.
// `streaming_writer` may be used from another thread (`Send`), but it's only going
// to be used by one thread at a time (`!Sync`).
//
//`element`
// must be valid and non-`NULL`. If `streaming_writer` is `NULL`, an error will be reported.
//
// Returns 0 on success.
int lol_html_element_streaming_prepend(lol_html_element_t *element,
lol_html_streaming_handler_t *streaming_writer);

//[`Element::streaming_append`]
//
// The [`CStreamingHandler`] contains callbacks that will be called
// when the content needs to be written.
//
// `streaming_writer` is copied immediately, and doesn't have a stable address.
// `streaming_writer` may be used from another thread (`Send`), but it's only going
// to be used by one thread at a time (`!Sync`).
//
//`element`
// must be valid and non-`NULL`. If `streaming_writer` is `NULL`, an error will be reported.
//
// Returns 0 on success.
int lol_html_element_streaming_append(lol_html_element_t *element,
lol_html_streaming_handler_t *streaming_writer);

//[`Element::streaming_before`]
//
// The [`CStreamingHandler`] contains callbacks that will be called
// when the content needs to be written.
//
// `streaming_writer` is copied immediately, and doesn't have a stable address.
// `streaming_writer` may be used from another thread (`Send`), but it's only going
// to be used by one thread at a time (`!Sync`).
//
//`element`
// must be valid and non-`NULL`. If `streaming_writer` is `NULL`, an error will be reported.
//
// Returns 0 on success.
int lol_html_element_streaming_before(lol_html_element_t *element,
lol_html_streaming_handler_t *streaming_writer);

//[`Element::streaming_after`]
//
// The [`CStreamingHandler`] contains callbacks that will be called
// when the content needs to be written.
//
// `streaming_writer` is copied immediately, and doesn't have a stable address.
// `streaming_writer` may be used from another thread (`Send`), but it's only going
// to be used by one thread at a time (`!Sync`).
//
//`element`
// must be valid and non-`NULL`. If `streaming_writer` is `NULL`, an error will be reported.
//
// Returns 0 on success.
int lol_html_element_streaming_after(lol_html_element_t *element,
lol_html_streaming_handler_t *streaming_writer);

//[`Element::streaming_set_inner_content`]
//
// The [`CStreamingHandler`] contains callbacks that will be called
// when the content needs to be written.
//
// `streaming_writer` is copied immediately, and doesn't have a stable address.
// `streaming_writer` may be used from another thread (`Send`), but it's only going
// to be used by one thread at a time (`!Sync`).
//
//`element`
// must be valid and non-`NULL`. If `streaming_writer` is `NULL`, an error will be reported.
//
// Returns 0 on success.
int lol_html_element_streaming_set_inner_content(lol_html_element_t *element,
lol_html_streaming_handler_t *streaming_writer);

//[`Element::streaming_replace`]
//
// The [`CStreamingHandler`] contains callbacks that will be called
// when the content needs to be written.
//
// `streaming_writer` is copied immediately, and doesn't have a stable address.
// `streaming_writer` may be used from another thread (`Send`), but it's only going
// to be used by one thread at a time (`!Sync`).
//
//`element`
// must be valid and non-`NULL`. If `streaming_writer` is `NULL`, an error will be reported.
//
// Returns 0 on success.
int lol_html_element_streaming_replace(lol_html_element_t *element,
lol_html_streaming_handler_t *streaming_writer);

//[`EndTag::streaming_before`]
//
// The [`CStreamingHandler`] contains callbacks that will be called
// when the content needs to be written.
//
// `streaming_writer` is copied immediately, and doesn't have a stable address.
// `streaming_writer` may be used from another thread (`Send`), but it's only going
// to be used by one thread at a time (`!Sync`).
//
//`end_tag`
// must be valid and non-`NULL`. If `streaming_writer` is `NULL`, an error will be reported.
//
// Returns 0 on success.
int lol_html_end_tag_streaming_before(lol_html_end_tag_t *end_tag,
lol_html_streaming_handler_t *streaming_writer);

//[`EndTag::streaming_after`]
//
// The [`CStreamingHandler`] contains callbacks that will be called
// when the content needs to be written.
//
// `streaming_writer` is copied immediately, and doesn't have a stable address.
// `streaming_writer` may be used from another thread (`Send`), but it's only going
// to be used by one thread at a time (`!Sync`).
//
//`end_tag`
// must be valid and non-`NULL`. If `streaming_writer` is `NULL`, an error will be reported.
//
// Returns 0 on success.
int lol_html_end_tag_streaming_after(lol_html_end_tag_t *end_tag,
lol_html_streaming_handler_t *streaming_writer);

//[`EndTag::streaming_replace`]
//
// The [`CStreamingHandler`] contains callbacks that will be called
// when the content needs to be written.
//
// `streaming_writer` is copied immediately, and doesn't have a stable address.
// `streaming_writer` may be used from another thread (`Send`), but it's only going
// to be used by one thread at a time (`!Sync`).
//
//`end_tag`
// must be valid and non-`NULL`. If `streaming_writer` is `NULL`, an error will be reported.
//
// Returns 0 on success.
int lol_html_end_tag_streaming_replace(lol_html_end_tag_t *end_tag,
lol_html_streaming_handler_t *streaming_writer);


//[`TextChunk::streaming_before`]
//
// The [`CStreamingHandler`] contains callbacks that will be called
// when the content needs to be written.
//
// `streaming_writer` is copied immediately, and doesn't have a stable address.
// `streaming_writer` may be used from another thread (`Send`), but it's only going
// to be used by one thread at a time (`!Sync`).
//
//`text_chunk`
// must be valid and non-`NULL`. If `streaming_writer` is `NULL`, an error will be reported.
//
// Returns 0 on success.
int lol_html_text_chunk_streaming_before(lol_html_text_chunk_t *text_chunk,
lol_html_streaming_handler_t *streaming_writer);

//[`TextChunk::streaming_after`]
//
// The [`CStreamingHandler`] contains callbacks that will be called
// when the content needs to be written.
//
// `streaming_writer` is copied immediately, and doesn't have a stable address.
// `streaming_writer` may be used from another thread (`Send`), but it's only going
// to be used by one thread at a time (`!Sync`).
//
//`text_chunk`
// must be valid and non-`NULL`. If `streaming_writer` is `NULL`, an error will be reported.
//
// Returns 0 on success.
int lol_html_text_chunk_streaming_after(lol_html_text_chunk_t *text_chunk,
lol_html_streaming_handler_t *streaming_writer);

//[`TextChunk::streaming_replace`]
//
// The [`CStreamingHandler`] contains callbacks that will be called
// when the content needs to be written.
//
// `streaming_writer` is copied immediately, and doesn't have a stable address.
// `streaming_writer` may be used from another thread (`Send`), but it's only going
// to be used by one thread at a time (`!Sync`).
//
//`text_chunk`
// must be valid and non-`NULL`. If `streaming_writer` is `NULL`, an error will be reported.
//
// Returns 0 on success.
int lol_html_text_chunk_streaming_replace(lol_html_text_chunk_t *text_chunk,
lol_html_streaming_handler_t *streaming_writer);

// Write another piece of UTF-8 data to the output. Returns `0` on success, and `-1` if it wasn't valid UTF-8.
// All pointers must be non-NULL.
int lol_html_streaming_sink_write_str(lol_html_streaming_sink_t *sink,
const char *string_utf8,
size_t string_utf8_len,
bool is_html);


#if defined(__cplusplus)
} // extern C
#endif
Expand Down
Loading

0 comments on commit 212155d

Please sign in to comment.