Skip to content

Commit 6dae0c6

Browse files
committed
C API for streaming content mutations
1 parent ce6ff71 commit 6dae0c6

File tree

12 files changed

+626
-220
lines changed

12 files changed

+626
-220
lines changed

c-api/c-tests/src/test.c

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,9 @@ int run_tests() {
1616
subtest("Element API", element_api_test);
1717
subtest("Document end API", document_end_api_test);
1818
subtest("Memory limiting", test_memory_limiting);
19-
return done_testing();
19+
int res = done_testing();
20+
if (res) {
21+
fprintf(stderr, "\nSome tests have failed\n");
22+
}
23+
return res;
2024
}

c-api/c-tests/src/test_element_api.c

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,114 @@ static void test_insert_content_around_element(lol_html_selector_t *selector, vo
238238
);
239239
}
240240

241+
//-------------------------------------------------------------------------
242+
EXPECT_OUTPUT(
243+
streaming_mutations_output_sink,
244+
"&amp;before<div><!--prepend-->Hi<!--append--></div>&amp;after\xf0\x9f\x98\x82",
245+
&EXPECTED_USER_DATA,
246+
sizeof(EXPECTED_USER_DATA)
247+
);
248+
249+
static void loltest_drop(void *user_data) {
250+
int *drops = user_data;
251+
(*drops)++;
252+
}
253+
254+
static int loltest_write_all_callback_before(lol_html_streaming_sink_t *sink, void *user_data) {
255+
int *counter = user_data;
256+
ok(*counter >= 100 && *counter <= 103);
257+
258+
const char *before = "&before";
259+
return lol_html_streaming_sink_write_str(sink, before, strlen(before), false);
260+
}
261+
262+
static int loltest_write_all_callback_after(lol_html_streaming_sink_t *sink, void *user_data) {
263+
int *counter = user_data;
264+
ok(*counter >= 100 && *counter <= 103);
265+
266+
const char *after = "&after";
267+
const char emoji[] = {0xf0,0x9f,0x98,0x82};
268+
return lol_html_streaming_sink_write_str(sink, after, strlen(after), false) ||
269+
lol_html_streaming_sink_write_str(sink, emoji, 4, false);
270+
}
271+
272+
static int loltest_write_all_callback_prepend(lol_html_streaming_sink_t *sink, void *user_data) {
273+
int *counter = user_data;
274+
ok(*counter >= 100 && *counter <= 103);
275+
276+
const char *prepend1 = "<!--pre";
277+
const char *prepend2 = "pend-->";
278+
return lol_html_streaming_sink_write_str(sink, prepend1, strlen(prepend1), true) ||
279+
lol_html_streaming_sink_write_str(sink, prepend2, strlen(prepend2), true);
280+
}
281+
282+
static int loltest_write_all_callback_append(lol_html_streaming_sink_t *sink, void *user_data) {
283+
int *counter = user_data;
284+
ok(*counter >= 100 && *counter <= 103);
285+
286+
const char *append = "<!--append-->";
287+
return lol_html_streaming_sink_write_str(sink, append, strlen(append), true);
288+
}
289+
290+
static lol_html_rewriter_directive_t streaming_mutations_around_element(
291+
lol_html_element_t *element,
292+
void *user_data
293+
) {
294+
note("Stream before/prepend");
295+
ok(!lol_html_element_streaming_before(element, &(lol_html_streaming_handler_t){
296+
.write_all_callback = loltest_write_all_callback_before,
297+
.user_data = user_data,
298+
.drop_callback = loltest_drop,
299+
}));
300+
ok(!lol_html_element_streaming_prepend(element, &(lol_html_streaming_handler_t){
301+
.write_all_callback = loltest_write_all_callback_prepend,
302+
.user_data = user_data,
303+
// tests null drop callback
304+
}));
305+
note("Stream after/append");
306+
ok(!lol_html_element_streaming_append(element, &(lol_html_streaming_handler_t){
307+
.write_all_callback = loltest_write_all_callback_append,
308+
.user_data = user_data,
309+
.drop_callback = loltest_drop,
310+
}));
311+
ok(!lol_html_element_streaming_after(element, &(lol_html_streaming_handler_t){
312+
.write_all_callback = loltest_write_all_callback_after,
313+
.user_data = user_data,
314+
.drop_callback = loltest_drop,
315+
}));
316+
317+
return LOL_HTML_CONTINUE;
318+
}
319+
320+
static void test_streaming_mutations_around_element(lol_html_selector_t *selector, void *user_data) {
321+
UNUSED(user_data);
322+
lol_html_rewriter_builder_t *builder = lol_html_rewriter_builder_new();
323+
324+
int drop_count = 100;
325+
326+
int err = lol_html_rewriter_builder_add_element_content_handlers(
327+
builder,
328+
selector,
329+
&streaming_mutations_around_element,
330+
&drop_count,
331+
NULL,
332+
NULL,
333+
NULL,
334+
NULL
335+
);
336+
337+
ok(!err);
338+
339+
run_rewriter(
340+
builder,
341+
"<div>Hi</div>",
342+
streaming_mutations_output_sink,
343+
user_data
344+
);
345+
346+
ok(drop_count == 103); // one has no drop callback on purpose
347+
}
348+
241349
//-------------------------------------------------------------------------
242350
EXPECT_OUTPUT(
243351
set_element_inner_content_output_sink,
@@ -706,6 +814,7 @@ void element_api_test() {
706814
test_iterate_attributes(selector, &user_data);
707815
test_get_and_modify_attributes(selector, &user_data);
708816
test_insert_content_around_element(selector, &user_data);
817+
test_streaming_mutations_around_element(selector, &user_data);
709818

710819
lol_html_selector_free(selector);
711820
}

c-api/cbindgen.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# To generate a header:
2+
#
3+
# cargo expand > tmp.rs
4+
# cbindgen tmp.rs
5+
6+
language = "C"
7+
tab_width = 4
8+
documentation = true
9+
documentation_style = "c99"
10+
documentation_length = "full"
11+
12+
[export]
13+
prefix = "lol_html_"
14+
15+
[export.mangle]
16+
rename_types = "SnakeCase"

c-api/include/lol_html.h

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ typedef struct lol_html_Element lol_html_element_t;
3030
typedef struct lol_html_AttributesIterator lol_html_attributes_iterator_t;
3131
typedef struct lol_html_Attribute lol_html_attribute_t;
3232
typedef struct lol_html_Selector lol_html_selector_t;
33+
typedef struct lol_html_CStreamingHandlerSink lol_html_streaming_sink_t;
3334

3435
// Library-allocated UTF8 string fat pointer.
3536
//
@@ -116,6 +117,30 @@ typedef lol_html_rewriter_directive_t (*lol_html_end_tag_handler_t)(
116117
void *user_data
117118
);
118119

120+
// For use with streaming content handlers.
121+
//
122+
// 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).
123+
// It doesn't have to be `Sync`, it will be used only by one thread at a time.
124+
//
125+
// Handler functions copy this struct. It can (and should) be created on the stack.
126+
typedef struct lol_html_CStreamingHandler {
127+
// Anything you like
128+
void *user_data;
129+
// Called when the handler is supposed to produce its output. Return `0` for success.
130+
// 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.
131+
// The sink is for [`lol_html_streaming_sink_write_str`].
132+
// `user_data` comes from this struct.
133+
//
134+
// `write_all_callback` must not be `NULL`.
135+
int (*write_all_callback)(lol_html_streaming_sink_t *sink, void *user_data);
136+
// Called exactly once, after the last use of this handler.
137+
// It may be `NULL`.
138+
// `user_data` comes from this struct.
139+
void (*drop_callback)(void *user_data);
140+
// *Always* initialize to `NULL`.
141+
void *reserved;
142+
} lol_html_streaming_handler_t;
143+
119144
// Selector
120145
//---------------------------------------------------------------------
121146

@@ -792,6 +817,209 @@ int lol_html_doc_end_append(
792817
bool is_html
793818
);
794819

820+
821+
822+
//[`Element::streaming_prepend`]
823+
//
824+
// The [`CStreamingHandler`] contains callbacks that will be called
825+
// when the content needs to be written.
826+
//
827+
// `streaming_writer` is copied immediately, and doesn't have a stable address.
828+
// `streaming_writer` may be used from another thread (`Send`), but it's only going
829+
// to be used by one thread at a time (`!Sync`).
830+
//
831+
//`element`
832+
// must be valid and non-`NULL`. If `streaming_writer` is `NULL`, an error will be reported.
833+
//
834+
// Returns 0 on success.
835+
int lol_html_element_streaming_prepend(lol_html_element_t *element,
836+
lol_html_streaming_handler_t *streaming_writer);
837+
838+
//[`Element::streaming_append`]
839+
//
840+
// The [`CStreamingHandler`] contains callbacks that will be called
841+
// when the content needs to be written.
842+
//
843+
// `streaming_writer` is copied immediately, and doesn't have a stable address.
844+
// `streaming_writer` may be used from another thread (`Send`), but it's only going
845+
// to be used by one thread at a time (`!Sync`).
846+
//
847+
//`element`
848+
// must be valid and non-`NULL`. If `streaming_writer` is `NULL`, an error will be reported.
849+
//
850+
// Returns 0 on success.
851+
int lol_html_element_streaming_append(lol_html_element_t *element,
852+
lol_html_streaming_handler_t *streaming_writer);
853+
854+
//[`Element::streaming_before`]
855+
//
856+
// The [`CStreamingHandler`] contains callbacks that will be called
857+
// when the content needs to be written.
858+
//
859+
// `streaming_writer` is copied immediately, and doesn't have a stable address.
860+
// `streaming_writer` may be used from another thread (`Send`), but it's only going
861+
// to be used by one thread at a time (`!Sync`).
862+
//
863+
//`element`
864+
// must be valid and non-`NULL`. If `streaming_writer` is `NULL`, an error will be reported.
865+
//
866+
// Returns 0 on success.
867+
int lol_html_element_streaming_before(lol_html_element_t *element,
868+
lol_html_streaming_handler_t *streaming_writer);
869+
870+
//[`Element::streaming_after`]
871+
//
872+
// The [`CStreamingHandler`] contains callbacks that will be called
873+
// when the content needs to be written.
874+
//
875+
// `streaming_writer` is copied immediately, and doesn't have a stable address.
876+
// `streaming_writer` may be used from another thread (`Send`), but it's only going
877+
// to be used by one thread at a time (`!Sync`).
878+
//
879+
//`element`
880+
// must be valid and non-`NULL`. If `streaming_writer` is `NULL`, an error will be reported.
881+
//
882+
// Returns 0 on success.
883+
int lol_html_element_streaming_after(lol_html_element_t *element,
884+
lol_html_streaming_handler_t *streaming_writer);
885+
886+
//[`Element::streaming_set_inner_content`]
887+
//
888+
// The [`CStreamingHandler`] contains callbacks that will be called
889+
// when the content needs to be written.
890+
//
891+
// `streaming_writer` is copied immediately, and doesn't have a stable address.
892+
// `streaming_writer` may be used from another thread (`Send`), but it's only going
893+
// to be used by one thread at a time (`!Sync`).
894+
//
895+
//`element`
896+
// must be valid and non-`NULL`. If `streaming_writer` is `NULL`, an error will be reported.
897+
//
898+
// Returns 0 on success.
899+
int lol_html_element_streaming_set_inner_content(lol_html_element_t *element,
900+
lol_html_streaming_handler_t *streaming_writer);
901+
902+
//[`Element::streaming_replace`]
903+
//
904+
// The [`CStreamingHandler`] contains callbacks that will be called
905+
// when the content needs to be written.
906+
//
907+
// `streaming_writer` is copied immediately, and doesn't have a stable address.
908+
// `streaming_writer` may be used from another thread (`Send`), but it's only going
909+
// to be used by one thread at a time (`!Sync`).
910+
//
911+
//`element`
912+
// must be valid and non-`NULL`. If `streaming_writer` is `NULL`, an error will be reported.
913+
//
914+
// Returns 0 on success.
915+
int lol_html_element_streaming_replace(lol_html_element_t *element,
916+
lol_html_streaming_handler_t *streaming_writer);
917+
918+
//[`EndTag::streaming_before`]
919+
//
920+
// The [`CStreamingHandler`] contains callbacks that will be called
921+
// when the content needs to be written.
922+
//
923+
// `streaming_writer` is copied immediately, and doesn't have a stable address.
924+
// `streaming_writer` may be used from another thread (`Send`), but it's only going
925+
// to be used by one thread at a time (`!Sync`).
926+
//
927+
//`end_tag`
928+
// must be valid and non-`NULL`. If `streaming_writer` is `NULL`, an error will be reported.
929+
//
930+
// Returns 0 on success.
931+
int lol_html_end_tag_streaming_before(lol_html_end_tag_t *end_tag,
932+
lol_html_streaming_handler_t *streaming_writer);
933+
934+
//[`EndTag::streaming_after`]
935+
//
936+
// The [`CStreamingHandler`] contains callbacks that will be called
937+
// when the content needs to be written.
938+
//
939+
// `streaming_writer` is copied immediately, and doesn't have a stable address.
940+
// `streaming_writer` may be used from another thread (`Send`), but it's only going
941+
// to be used by one thread at a time (`!Sync`).
942+
//
943+
//`end_tag`
944+
// must be valid and non-`NULL`. If `streaming_writer` is `NULL`, an error will be reported.
945+
//
946+
// Returns 0 on success.
947+
int lol_html_end_tag_streaming_after(lol_html_end_tag_t *end_tag,
948+
lol_html_streaming_handler_t *streaming_writer);
949+
950+
//[`EndTag::streaming_replace`]
951+
//
952+
// The [`CStreamingHandler`] contains callbacks that will be called
953+
// when the content needs to be written.
954+
//
955+
// `streaming_writer` is copied immediately, and doesn't have a stable address.
956+
// `streaming_writer` may be used from another thread (`Send`), but it's only going
957+
// to be used by one thread at a time (`!Sync`).
958+
//
959+
//`end_tag`
960+
// must be valid and non-`NULL`. If `streaming_writer` is `NULL`, an error will be reported.
961+
//
962+
// Returns 0 on success.
963+
int lol_html_end_tag_streaming_replace(lol_html_end_tag_t *end_tag,
964+
lol_html_streaming_handler_t *streaming_writer);
965+
966+
967+
//[`TextChunk::streaming_before`]
968+
//
969+
// The [`CStreamingHandler`] contains callbacks that will be called
970+
// when the content needs to be written.
971+
//
972+
// `streaming_writer` is copied immediately, and doesn't have a stable address.
973+
// `streaming_writer` may be used from another thread (`Send`), but it's only going
974+
// to be used by one thread at a time (`!Sync`).
975+
//
976+
//`text_chunk`
977+
// must be valid and non-`NULL`. If `streaming_writer` is `NULL`, an error will be reported.
978+
//
979+
// Returns 0 on success.
980+
int lol_html_text_chunk_streaming_before(lol_html_text_chunk_t *text_chunk,
981+
lol_html_streaming_handler_t *streaming_writer);
982+
983+
//[`TextChunk::streaming_after`]
984+
//
985+
// The [`CStreamingHandler`] contains callbacks that will be called
986+
// when the content needs to be written.
987+
//
988+
// `streaming_writer` is copied immediately, and doesn't have a stable address.
989+
// `streaming_writer` may be used from another thread (`Send`), but it's only going
990+
// to be used by one thread at a time (`!Sync`).
991+
//
992+
//`text_chunk`
993+
// must be valid and non-`NULL`. If `streaming_writer` is `NULL`, an error will be reported.
994+
//
995+
// Returns 0 on success.
996+
int lol_html_text_chunk_streaming_after(lol_html_text_chunk_t *text_chunk,
997+
lol_html_streaming_handler_t *streaming_writer);
998+
999+
//[`TextChunk::streaming_replace`]
1000+
//
1001+
// The [`CStreamingHandler`] contains callbacks that will be called
1002+
// when the content needs to be written.
1003+
//
1004+
// `streaming_writer` is copied immediately, and doesn't have a stable address.
1005+
// `streaming_writer` may be used from another thread (`Send`), but it's only going
1006+
// to be used by one thread at a time (`!Sync`).
1007+
//
1008+
//`text_chunk`
1009+
// must be valid and non-`NULL`. If `streaming_writer` is `NULL`, an error will be reported.
1010+
//
1011+
// Returns 0 on success.
1012+
int lol_html_text_chunk_streaming_replace(lol_html_text_chunk_t *text_chunk,
1013+
lol_html_streaming_handler_t *streaming_writer);
1014+
1015+
// Write another piece of UTF-8 data to the output. Returns `0` on success, and `-1` if it wasn't valid UTF-8.
1016+
// All pointers must be non-NULL.
1017+
int lol_html_streaming_sink_write_str(lol_html_streaming_sink_t *sink,
1018+
const char *string_utf8,
1019+
size_t string_utf8_len,
1020+
bool is_html);
1021+
1022+
7951023
#if defined(__cplusplus)
7961024
} // extern C
7971025
#endif

0 commit comments

Comments
 (0)