Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Streaming handlers #229

Merged
merged 15 commits into from
Nov 29, 2024
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
Loading