diff --git a/internal/cbm/extract_imports.c b/internal/cbm/extract_imports.c index 961244180..4e44d33ac 100644 --- a/internal/cbm/extract_imports.c +++ b/internal/cbm/extract_imports.c @@ -1,6 +1,7 @@ #include "cbm.h" #include "arena.h" // CBMArena, cbm_arena_strdup/strndup/sprintf #include "helpers.h" +#include "lang_specs.h" // CBMLangSpec, CBMEmbeddedLangSpec, cbm_lang_spec, cbm_ts_language #include "tree_sitter/api.h" // TSNode, ts_node_* #include "foundation/constants.h" #include "ts_node_stack.h" @@ -846,6 +847,99 @@ static void parse_wolfram_imports(CBMExtractCtx *ctx) { walk_wolfram_imports(ctx, ctx->root); } +// --- Embedded-language imports --- +// Generic walker for host grammars (Svelte, Vue, HTML, Astro, ...) whose AST +// keeps embedded sub-language source as raw_text (or similar) without parsing +// it. The host's CBMLangSpec.embedded_imports declares which content nodes +// hold which sub-language; we re-parse each match with the embedded grammar +// and run the standard ES import walker over the inner AST. +// +// No grammar symbols are referenced here — the embedded TSLanguage is +// resolved through cbm_ts_language(spec->embedded_language), the same hook +// that the main parser uses. Adding another host language is a one-line +// declaration in lang_specs.c. + +static void embedded_collect_content_nodes(TSNode root, const CBMEmbeddedLangSpec *spec, + TSNode *out, int *out_count, int max_out) { + /* Iterative DFS so deeply-nested script blocks are still found. Cap the + * stack to a sane bound (host grammars do not have million-deep markup + * trees) — no need to introduce TSNodeStack here. */ + enum { EMBED_STACK_CAP = 1024 }; + TSNode stack[EMBED_STACK_CAP]; + int top = 0; + stack[top++] = root; + while (top > 0 && *out_count < max_out) { + TSNode node = stack[--top]; + const char *kind = ts_node_type(node); + if (strcmp(kind, spec->script_node_type) == 0) { + uint32_t cc = ts_node_child_count(node); + for (uint32_t k = 0; k < cc; k++) { + TSNode c = ts_node_child(node, k); + if (strcmp(ts_node_type(c), spec->content_node_type) == 0) { + out[(*out_count)++] = c; + if (*out_count >= max_out) { + return; + } + break; /* one content node per script element */ + } + } + /* Do not descend into \n" + "

Hello {value}

\n", + CBM_LANG_SVELTE, "t", "Comp.svelte"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT_GTE(r->imports.count, 3); + ASSERT(has_import(r, "Foo.svelte")); + ASSERT(has_import(r, "lib/utils")); + ASSERT(has_import(r, "helpers")); + cbm_free_result(r); + PASS(); +} + +TEST(svelte_imports_no_script) { + /* .svelte with no \n", + CBM_LANG_VUE, "t", "App.vue"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT_GTE(r->imports.count, 2); + ASSERT(has_import(r, "MyComp.vue")); + ASSERT(has_import(r, "vue")); + cbm_free_result(r); + PASS(); +} + +TEST(html_imports_basic) { + /* Plain HTML with inline ES module imports — same generic walker. */ + CBMFileResult *r = extract( + "\n" + "\n" + "\n", + CBM_LANG_HTML, "t", "index.html"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT_GTE(r->imports.count, 2); + ASSERT(has_import(r, "app.js")); + ASSERT(has_import(r, "utils.js")); + cbm_free_result(r); + PASS(); +} + /* ═══════════════════════════════════════════════════════════════════ * config_extraction_test.go ports (25 tests) * ═══════════════════════════════════════════════════════════════════ */ @@ -2297,6 +2378,10 @@ SUITE(extraction) { RUN_TEST(ruby_imports); RUN_TEST(lua_imports); RUN_TEST(import_stress_go); + RUN_TEST(svelte_imports_basic); + RUN_TEST(svelte_imports_no_script); + RUN_TEST(vue_imports_basic); + RUN_TEST(html_imports_basic); /* config_extraction_test.go ports */ RUN_TEST(toml_basic_table_and_pair);