changeset 156:cd35e600ae34

[MarkDown Converter] Fixed few things and made a test
author June Park <parkjune1995@gmail.com>
date Mon, 12 Jan 2026 15:20:39 -0800
parents 3bb45eb67906
children 2db6253f355d
files gui_ze/gui_ze.bzl markdown_converter/BUILD markdown_converter/markdown_to_html.c markdown_converter/markdown_to_html.h markdown_converter/markdown_to_html_wasm.c markdown_converter/markdown_to_html_wasm.js markdown_converter/tests/BUILD markdown_converter/tests/test_wasm.js markdown_converter/wasm/BUILD markdown_converter/wasm/markdown_to_html_wasm.c markdown_converter/wasm/markdown_to_html_wasm.js
diffstat 11 files changed, 1152 insertions(+), 788 deletions(-) [+]
line wrap: on
line diff
--- a/gui_ze/gui_ze.bzl	Mon Jan 12 09:12:31 2026 -0800
+++ b/gui_ze/gui_ze.bzl	Mon Jan 12 15:20:39 2026 -0800
@@ -203,6 +203,69 @@
   },
 )
 
+def _bun_run_impl(ctx):
+    actual_exe = ctx.actions.declare_file(ctx.label.name)
+    
+    # 1. Get the workspace name (crucial for runfiles paths)
+    workspace_name = ctx.workspace_name
+
+    # 2. Define the paths relative to the runfiles root
+    bun_path = ctx.executable._bun.short_path
+    src_path = ctx.file.src.short_path
+
+    # 3. Create the launcher script
+    # We use a template to handle the environment and pathing
+    script_content = """#!/bin/bash
+# Find the runfiles directory
+if [[ -z "$RUNFILES_DIR" ]]; then
+  if [[ -d "$0.runfiles" ]]; then
+    RUNFILES_DIR="$0.runfiles"
+  fi
+fi
+
+# Navigate to the workspace root inside runfiles
+cd "$RUNFILES_DIR/{workspace}"
+pwd
+# Execute bun with the src file and any extra arguments
+exec "./{bun_bin}" run "./{src_file}" "$@"
+""".format(
+        workspace = workspace_name,
+        bun_bin = bun_path,
+        src_file = src_path
+    )
+
+    ctx.actions.write(
+        output = actual_exe,
+        content = script_content,
+        is_executable = True,
+    )
+
+    return [
+        DefaultInfo(
+            executable = actual_exe,
+            runfiles = ctx.runfiles(
+                files = [ctx.file.src] + ctx.files.data + ctx.files._bun_files
+            ).merge(ctx.attr._bun[DefaultInfo].default_runfiles),
+        ),
+    ]
+
+# TODO: Fix the rules so that it can do relative import.
+bun_run = rule(
+  implementation = _bun_run_impl,
+  executable = True,
+  attrs = {
+    "src": attr.label(allow_single_file = True, mandatory = True),
+    "data": attr.label_list(allow_files = True),
+    "_bun": attr.label(
+      default = Label("//third_party/bun:bun"), 
+      executable = True, 
+      cfg = "exec"
+    ),
+    "_bun_files": attr.label(default = Label("//third_party/bun:bun_files")),
+  },
+)
+
+
 def _move_files_into_dir_impl(ctx):
   srcs = ctx.files.srcs
   outs = []
--- a/markdown_converter/BUILD	Mon Jan 12 09:12:31 2026 -0800
+++ b/markdown_converter/BUILD	Mon Jan 12 15:20:39 2026 -0800
@@ -1,7 +1,7 @@
 load("@rules_cc//cc:cc_library.bzl", "cc_library")
-load("//gui_ze:gui_ze.bzl", "wasm_binary")
+load("//gui_ze:gui_ze.bzl", "wasm_binary", "bun_run", "move_files_into_dir")
 
-# JavaScript implementation (original)
+# JS only 
 filegroup(
   name = "markdown_to_html",
   srcs = glob([
@@ -14,22 +14,5 @@
 cc_library(
   name = "markdown_to_html_c",
   srcs = ["markdown_to_html.c"],
-  hdrs = ["markdown_to_html.h"],
   visibility = ["//visibility:public"],
 )
-
-# WASM binary for browser FFI
-wasm_binary(
-  name = "markdown_to_html_wasm",
-  src = "markdown_to_html_wasm.c",
-  exports = [
-    "malloc",
-    "free",
-    "heap_reset",
-    "markdown_to_html",
-    "markdown_strlen",
-  ],
-  visibility = ["//visibility:public"],
-)
-
-
--- a/markdown_converter/markdown_to_html.c	Mon Jan 12 09:12:31 2026 -0800
+++ b/markdown_converter/markdown_to_html.c	Mon Jan 12 15:20:39 2026 -0800
@@ -3,7 +3,66 @@
  * Supports: headers, bold, italic, links, lists, code blocks, blockquotes, horizontal rules
  */
 
-#include "markdown_to_html.h"
+#ifndef MARKDOWN_TO_HTML_H
+#define MARKDOWN_TO_HTML_H
+
+#include <stddef.h>
+
+// Export macro for WASM/Emscripten
+#ifdef __EMSCRIPTEN__
+  #include <emscripten.h>
+  #define MDAPI EMSCRIPTEN_KEEPALIVE
+#else
+  #ifdef _WIN32
+    #ifdef MARKDOWN_EXPORTS
+      #define MDAPI __declspec(dllexport)
+    #else
+      #define MDAPI __declspec(dllimport)
+    #endif
+  #else
+    #define MDAPI extern
+  #endif
+#endif
+
+/**
+ * Convert markdown string to HTML string.
+ *
+ * @param markdown The input markdown string (null-terminated)
+ * @return Newly allocated HTML string. Caller must free with markdown_free().
+ *         Returns NULL on allocation failure.
+ *
+ * Supported markdown features:
+ * - Headers: # H1, ## H2, ... ###### H6
+ * - Bold: **text** or __text__
+ * - Italic: *text* or _text_
+ * - Strikethrough: ~~text~~
+ * - Links: [text](url)
+ * - Images: ![alt](url)
+ * - Inline code: `code`
+ * - Code blocks: ```code```
+ * - Unordered lists: -, *, +
+ * - Ordered lists: 1., 2., etc.
+ * - Blockquotes: > text
+ * - Horizontal rules: ---, ***, ___
+ */
+MDAPI char *markdown_to_html(const char *markdown);
+
+/**
+ * Free HTML string returned by markdown_to_html.
+ *
+ * @param html The HTML string to free
+ */
+MDAPI void markdown_free(char *html);
+
+/**
+ * Get length of HTML string (useful for WASM memory operations).
+ *
+ * @param html The HTML string
+ * @return Length of the string, or 0 if NULL
+ */
+MDAPI size_t markdown_get_length(const char *html);
+
+#endif // MARKDOWN_TO_HTML_H
 #include <string.h>
 #include <stdlib.h>
 #include <stdio.h>
--- a/markdown_converter/markdown_to_html.h	Mon Jan 12 09:12:31 2026 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,60 +0,0 @@
-#ifndef MARKDOWN_TO_HTML_H
-#define MARKDOWN_TO_HTML_H
-
-#include <stddef.h>
-
-// Export macro for WASM/Emscripten
-#ifdef __EMSCRIPTEN__
-  #include <emscripten.h>
-  #define MDAPI EMSCRIPTEN_KEEPALIVE
-#else
-  #ifdef _WIN32
-    #ifdef MARKDOWN_EXPORTS
-      #define MDAPI __declspec(dllexport)
-    #else
-      #define MDAPI __declspec(dllimport)
-    #endif
-  #else
-    #define MDAPI extern
-  #endif
-#endif
-
-/**
- * Convert markdown string to HTML string.
- *
- * @param markdown The input markdown string (null-terminated)
- * @return Newly allocated HTML string. Caller must free with markdown_free().
- *         Returns NULL on allocation failure.
- *
- * Supported markdown features:
- * - Headers: # H1, ## H2, ... ###### H6
- * - Bold: **text** or __text__
- * - Italic: *text* or _text_
- * - Strikethrough: ~~text~~
- * - Links: [text](url)
- * - Images: ![alt](url)
- * - Inline code: `code`
- * - Code blocks: ```code```
- * - Unordered lists: -, *, +
- * - Ordered lists: 1., 2., etc.
- * - Blockquotes: > text
- * - Horizontal rules: ---, ***, ___
- */
-MDAPI char *markdown_to_html(const char *markdown);
-
-/**
- * Free HTML string returned by markdown_to_html.
- *
- * @param html The HTML string to free
- */
-MDAPI void markdown_free(char *html);
-
-/**
- * Get length of HTML string (useful for WASM memory operations).
- *
- * @param html The HTML string
- * @return Length of the string, or 0 if NULL
- */
-MDAPI size_t markdown_get_length(const char *html);
-
-#endif // MARKDOWN_TO_HTML_H
--- a/markdown_converter/markdown_to_html_wasm.c	Mon Jan 12 09:12:31 2026 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,565 +0,0 @@
-/**
- * Markdown to HTML Converter - Standalone WASM Implementation
- * No libc dependencies - can be compiled with: clang --target=wasm32
- */
-
-#define WASM_EXPORT __attribute__((visibility("default")))
-
-typedef unsigned long size_t;
-typedef int int32_t;
-
-// Simple bump allocator for WASM
-#define HEAP_SIZE (1024 * 1024)  // 1MB heap
-static char heap[HEAP_SIZE];
-static size_t heap_offset = 0;
-
-WASM_EXPORT void *malloc(size_t size)
-{
-  // Align to 8 bytes
-  size_t aligned_offset = (heap_offset + 7) & ~7;
-  if (aligned_offset + size > HEAP_SIZE) return 0;
-
-  void *ptr = &heap[aligned_offset];
-  heap_offset = aligned_offset + size;
-  return ptr;
-}
-
-WASM_EXPORT void free(void *ptr)
-{
-  // Simple bump allocator - no actual free
-  (void)ptr;
-}
-
-WASM_EXPORT void heap_reset(void)
-{
-  heap_offset = 0;
-}
-
-// String functions
-static size_t strlen(const char *s)
-{
-  size_t len = 0;
-  while (s[len]) len++;
-  return len;
-}
-
-static void *memcpy(void *dest, const void *src, size_t n)
-{
-  char *d = (char *)dest;
-  const char *s = (const char *)src;
-  while (n--) *d++ = *s++;
-  return dest;
-}
-
-static int isspace_c(int c)
-{
-  return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v';
-}
-
-static int isdigit_c(int c)
-{
-  return c >= '0' && c <= '9';
-}
-
-// String buffer for building HTML output
-typedef struct {
-  char  *data;
-  size_t length;
-  size_t capacity;
-} StringBuffer;
-
-static StringBuffer *buffer_create(size_t initial_capacity)
-{
-  StringBuffer *buf = (StringBuffer *)malloc(sizeof(StringBuffer));
-  if (!buf) return 0;
-
-  buf->data = (char *)malloc(initial_capacity);
-  if (!buf->data) return 0;
-
-  buf->data[0] = '\0';
-  buf->length = 0;
-  buf->capacity = initial_capacity;
-  return buf;
-}
-
-static void buffer_grow(StringBuffer *buf, size_t needed)
-{
-  if (buf->length + needed + 1 > buf->capacity) {
-    size_t new_capacity = buf->capacity * 2;
-    while (new_capacity < buf->length + needed + 1)
-      new_capacity *= 2;
-
-    char *new_data = (char *)malloc(new_capacity);
-    if (new_data) {
-      memcpy(new_data, buf->data, buf->length + 1);
-      buf->data = new_data;
-      buf->capacity = new_capacity;
-    }
-  }
-}
-
-static void buffer_append(StringBuffer *buf, const char *str)
-{
-  size_t len = strlen(str);
-  buffer_grow(buf, len);
-  memcpy(buf->data + buf->length, str, len + 1);
-  buf->length += len;
-}
-
-static void buffer_append_n(StringBuffer *buf, const char *str, size_t n)
-{
-  buffer_grow(buf, n);
-  memcpy(buf->data + buf->length, str, n);
-  buf->length += n;
-  buf->data[buf->length] = '\0';
-}
-
-static void buffer_append_char(StringBuffer *buf, char c)
-{
-  buffer_grow(buf, 1);
-  buf->data[buf->length++] = c;
-  buf->data[buf->length] = '\0';
-}
-
-// Check if line starts with pattern (after trimming whitespace)
-static int starts_with(const char *line, const char *pattern)
-{
-  while (*line && isspace_c(*line)) line++;
-  size_t plen = strlen(pattern);
-  for (size_t i = 0; i < plen; i++) {
-    if (line[i] != pattern[i]) return 0;
-  }
-  return 1;
-}
-
-// Count leading # characters
-static int count_heading_level(const char *line)
-{
-  int count = 0;
-  while (*line && isspace_c(*line)) line++;
-  while (line[count] == '#' && count < 6) count++;
-  if (count > 0 && line[count] == ' ') return count;
-  return 0;
-}
-
-// Skip whitespace
-static const char *skip_whitespace(const char *str)
-{
-  while (*str && isspace_c(*str)) str++;
-  return str;
-}
-
-// Check if line is empty
-static int is_empty_line(const char *line)
-{
-  while (*line) {
-    if (!isspace_c(*line)) return 0;
-    line++;
-  }
-  return 1;
-}
-
-// Check if line is horizontal rule
-static int is_horizontal_rule(const char *line)
-{
-  line = skip_whitespace(line);
-  char first = *line;
-  if (first != '-' && first != '*' && first != '_') return 0;
-
-  int count = 0;
-  while (*line) {
-    if (*line == first) count++;
-    else if (!isspace_c(*line)) return 0;
-    line++;
-  }
-  return count >= 3;
-}
-
-// Check if line is unordered list item
-static int is_unordered_list(const char *line)
-{
-  line = skip_whitespace(line);
-  return (*line == '-' || *line == '*' || *line == '+') && line[1] == ' ';
-}
-
-// Check if line is ordered list item
-static int is_ordered_list(const char *line)
-{
-  line = skip_whitespace(line);
-  while (*line && isdigit_c(*line)) line++;
-  return *line == '.' && line[1] == ' ';
-}
-
-// Process inline markdown
-static void process_inline(StringBuffer *buf, const char *text, size_t len)
-{
-  size_t i = 0;
-
-  while (i < len) {
-    // Links: [text](url)
-    if (text[i] == '[') {
-      size_t link_start = i + 1;
-      size_t link_end = link_start;
-      while (link_end < len && text[link_end] != ']') link_end++;
-
-      if (link_end < len && link_end + 1 < len && text[link_end + 1] == '(') {
-        size_t url_start = link_end + 2;
-        size_t url_end = url_start;
-        while (url_end < len && text[url_end] != ')') url_end++;
-
-        if (url_end < len) {
-          buffer_append(buf, "<a href=\"");
-          buffer_append_n(buf, text + url_start, url_end - url_start);
-          buffer_append(buf, "\">");
-          buffer_append_n(buf, text + link_start, link_end - link_start);
-          buffer_append(buf, "</a>");
-          i = url_end + 1;
-          continue;
-        }
-      }
-    }
-
-    // Images: ![alt](url)
-    if (text[i] == '!' && i + 1 < len && text[i + 1] == '[') {
-      size_t alt_start = i + 2;
-      size_t alt_end = alt_start;
-      while (alt_end < len && text[alt_end] != ']') alt_end++;
-
-      if (alt_end < len && alt_end + 1 < len && text[alt_end + 1] == '(') {
-        size_t url_start = alt_end + 2;
-        size_t url_end = url_start;
-        while (url_end < len && text[url_end] != ')') url_end++;
-
-        if (url_end < len) {
-          buffer_append(buf, "<img src=\"");
-          buffer_append_n(buf, text + url_start, url_end - url_start);
-          buffer_append(buf, "\" alt=\"");
-          buffer_append_n(buf, text + alt_start, alt_end - alt_start);
-          buffer_append(buf, "\">");
-          i = url_end + 1;
-          continue;
-        }
-      }
-    }
-
-    // Bold: **text** or __text__
-    if ((text[i] == '*' && i + 1 < len && text[i + 1] == '*') ||
-        (text[i] == '_' && i + 1 < len && text[i + 1] == '_')) {
-      char marker = text[i];
-      size_t start = i + 2;
-      size_t end = start;
-      while (end + 1 < len && !(text[end] == marker && text[end + 1] == marker)) end++;
-
-      if (end + 1 < len) {
-        buffer_append(buf, "<strong>");
-        process_inline(buf, text + start, end - start);
-        buffer_append(buf, "</strong>");
-        i = end + 2;
-        continue;
-      }
-    }
-
-    // Strikethrough: ~~text~~
-    if (text[i] == '~' && i + 1 < len && text[i + 1] == '~') {
-      size_t start = i + 2;
-      size_t end = start;
-      while (end + 1 < len && !(text[end] == '~' && text[end + 1] == '~')) end++;
-
-      if (end + 1 < len) {
-        buffer_append(buf, "<del>");
-        process_inline(buf, text + start, end - start);
-        buffer_append(buf, "</del>");
-        i = end + 2;
-        continue;
-      }
-    }
-
-    // Italic: *text* or _text_
-    if ((text[i] == '*' || text[i] == '_') && i + 1 < len && !isspace_c(text[i + 1])) {
-      char marker = text[i];
-      size_t start = i + 1;
-      size_t end = start;
-      while (end < len && text[end] != marker) end++;
-
-      if (end < len && end > start) {
-        buffer_append(buf, "<em>");
-        process_inline(buf, text + start, end - start);
-        buffer_append(buf, "</em>");
-        i = end + 1;
-        continue;
-      }
-    }
-
-    // Inline code: `code`
-    if (text[i] == '`') {
-      size_t start = i + 1;
-      size_t end = start;
-      while (end < len && text[end] != '`') end++;
-
-      if (end < len) {
-        buffer_append(buf, "<code>");
-        buffer_append_n(buf, text + start, end - start);
-        buffer_append(buf, "</code>");
-        i = end + 1;
-        continue;
-      }
-    }
-
-    // HTML escape
-    if (text[i] == '<') {
-      buffer_append(buf, "&lt;");
-    } else if (text[i] == '>') {
-      buffer_append(buf, "&gt;");
-    } else if (text[i] == '&') {
-      buffer_append(buf, "&amp;");
-    } else {
-      buffer_append_char(buf, text[i]);
-    }
-    i++;
-  }
-}
-
-// Append heading tag
-static void append_heading_tag(StringBuffer *buf, int level, int closing)
-{
-  buffer_append_char(buf, '<');
-  if (closing) buffer_append_char(buf, '/');
-  buffer_append_char(buf, 'h');
-  buffer_append_char(buf, '0' + level);
-  buffer_append_char(buf, '>');
-}
-
-// Convert markdown to HTML
-WASM_EXPORT char *markdown_to_html(const char *markdown)
-{
-  if (!markdown) return 0;
-
-  StringBuffer *buf = buffer_create(4096);
-  if (!buf) return 0;
-
-  const char *ptr = markdown;
-  const char *line_start;
-
-  while (*ptr) {
-    line_start = ptr;
-
-    // Find end of line
-    while (*ptr && *ptr != '\n') ptr++;
-    size_t line_len = ptr - line_start;
-
-    // Create line copy
-    char *line = (char *)malloc(line_len + 1);
-    if (!line) return buf->data;
-    memcpy(line, line_start, line_len);
-    line[line_len] = '\0';
-
-    // Skip empty lines
-    if (is_empty_line(line)) {
-      if (*ptr == '\n') ptr++;
-      continue;
-    }
-
-    // Headings
-    int heading_level = count_heading_level(line);
-    if (heading_level > 0) {
-      const char *content = skip_whitespace(line);
-      while (*content == '#') content++;
-      content = skip_whitespace(content);
-
-      append_heading_tag(buf, heading_level, 0);
-      process_inline(buf, content, strlen(content));
-      append_heading_tag(buf, heading_level, 1);
-
-      if (*ptr == '\n') ptr++;
-      continue;
-    }
-
-    // Code block
-    if (starts_with(line, "```")) {
-      buffer_append(buf, "<pre><code>");
-      if (*ptr == '\n') ptr++;
-
-      while (*ptr) {
-        line_start = ptr;
-        while (*ptr && *ptr != '\n') ptr++;
-        line_len = ptr - line_start;
-
-        char *code_line = (char *)malloc(line_len + 1);
-        if (!code_line) break;
-        memcpy(code_line, line_start, line_len);
-        code_line[line_len] = '\0';
-
-        if (starts_with(code_line, "```")) {
-          if (*ptr == '\n') ptr++;
-          break;
-        }
-
-        for (size_t i = 0; i < line_len; i++) {
-          if (code_line[i] == '<') buffer_append(buf, "&lt;");
-          else if (code_line[i] == '>') buffer_append(buf, "&gt;");
-          else if (code_line[i] == '&') buffer_append(buf, "&amp;");
-          else buffer_append_char(buf, code_line[i]);
-        }
-        buffer_append_char(buf, '\n');
-
-        if (*ptr == '\n') ptr++;
-      }
-
-      buffer_append(buf, "</code></pre>");
-      continue;
-    }
-
-    // Blockquote
-    if (starts_with(line, ">")) {
-      buffer_append(buf, "<blockquote>");
-
-      while (1) {
-        const char *content = skip_whitespace(line);
-        if (*content == '>') content++;
-        content = skip_whitespace(content);
-        process_inline(buf, content, strlen(content));
-        buffer_append_char(buf, ' ');
-
-        if (*ptr == '\n') ptr++;
-        if (!*ptr) break;
-
-        line_start = ptr;
-        while (*ptr && *ptr != '\n') ptr++;
-        line_len = ptr - line_start;
-
-        line = (char *)malloc(line_len + 1);
-        if (!line) break;
-        memcpy(line, line_start, line_len);
-        line[line_len] = '\0';
-
-        if (!starts_with(line, ">")) {
-          ptr = line_start;
-          break;
-        }
-      }
-
-      buffer_append(buf, "</blockquote>");
-      continue;
-    }
-
-    // Horizontal rule
-    if (is_horizontal_rule(line)) {
-      buffer_append(buf, "<hr>");
-      if (*ptr == '\n') ptr++;
-      continue;
-    }
-
-    // Unordered list
-    if (is_unordered_list(line)) {
-      buffer_append(buf, "<ul>");
-
-      while (1) {
-        const char *content = skip_whitespace(line);
-        content += 2;
-
-        buffer_append(buf, "<li>");
-        process_inline(buf, content, strlen(content));
-        buffer_append(buf, "</li>");
-
-        if (*ptr == '\n') ptr++;
-        if (!*ptr) break;
-
-        line_start = ptr;
-        while (*ptr && *ptr != '\n') ptr++;
-        line_len = ptr - line_start;
-
-        line = (char *)malloc(line_len + 1);
-        if (!line) break;
-        memcpy(line, line_start, line_len);
-        line[line_len] = '\0';
-
-        if (!is_unordered_list(line)) {
-          ptr = line_start;
-          break;
-        }
-      }
-
-      buffer_append(buf, "</ul>");
-      continue;
-    }
-
-    // Ordered list
-    if (is_ordered_list(line)) {
-      buffer_append(buf, "<ol>");
-
-      while (1) {
-        const char *content = skip_whitespace(line);
-        while (*content && isdigit_c(*content)) content++;
-        if (*content == '.') content++;
-        content = skip_whitespace(content);
-
-        buffer_append(buf, "<li>");
-        process_inline(buf, content, strlen(content));
-        buffer_append(buf, "</li>");
-
-        if (*ptr == '\n') ptr++;
-        if (!*ptr) break;
-
-        line_start = ptr;
-        while (*ptr && *ptr != '\n') ptr++;
-        line_len = ptr - line_start;
-
-        line = (char *)malloc(line_len + 1);
-        if (!line) break;
-        memcpy(line, line_start, line_len);
-        line[line_len] = '\0';
-
-        if (!is_ordered_list(line)) {
-          ptr = line_start;
-          break;
-        }
-      }
-
-      buffer_append(buf, "</ol>");
-      continue;
-    }
-
-    // Paragraph
-    buffer_append(buf, "<p>");
-
-    while (1) {
-      const char *content = skip_whitespace(line);
-      process_inline(buf, content, strlen(content));
-
-      if (*ptr == '\n') ptr++;
-      if (!*ptr) break;
-
-      line_start = ptr;
-      while (*ptr && *ptr != '\n') ptr++;
-      line_len = ptr - line_start;
-
-      line = (char *)malloc(line_len + 1);
-      if (!line) break;
-      memcpy(line, line_start, line_len);
-      line[line_len] = '\0';
-
-      if (is_empty_line(line) ||
-          count_heading_level(line) > 0 ||
-          starts_with(line, "```") ||
-          starts_with(line, ">") ||
-          is_horizontal_rule(line) ||
-          is_unordered_list(line) ||
-          is_ordered_list(line)) {
-        ptr = line_start;
-        break;
-      }
-
-      buffer_append_char(buf, ' ');
-    }
-
-    buffer_append(buf, "</p>");
-  }
-
-  return buf->data;
-}
-
-// Get string length (for JS interop)
-WASM_EXPORT size_t markdown_strlen(const char *str)
-{
-  return str ? strlen(str) : 0;
-}
--- a/markdown_converter/markdown_to_html_wasm.js	Mon Jan 12 09:12:31 2026 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,143 +0,0 @@
-/**
- * Markdown to HTML Converter - WASM FFI Wrapper
- * Loads the C implementation via WebAssembly and provides JavaScript API
- *
- * Build the WASM with: bazel build //markdown_converter:markdown_to_html_wasm
- */
-
-class MarkdownConverterWasm {
-  constructor() {
-    this.wasm = undefined;
-    this.ready = false;
-  }
-
-  /**
-   * Initialize the WASM module
-   * @param {string} wasmPath - Path to the .wasm file
-   * @returns {Promise<void>}
-   */
-  async init(wasmPath = '/markdown_to_html_wasm.wasm') {
-    if (this.ready) return;
-
-    try {
-      this.wasm = await WebAssembly.instantiateStreaming(fetch(wasmPath), {
-        env: {}
-      });
-      this.ready = true;
-    } catch (err) {
-      console.error('Failed to load markdown WASM module:', err);
-      throw err;
-    }
-  }
-
-  /**
-   * Write string to WASM memory
-   * @private
-   */
-  _writeString(str) {
-    const encoder = new TextEncoder();
-    const bytes = encoder.encode(str + '\0');
-    const ptr = this.wasm.instance.exports.malloc(bytes.length);
-    const memory = new Uint8Array(this.wasm.instance.exports.memory.buffer);
-    memory.set(bytes, ptr);
-    return ptr;
-  }
-
-  /**
-   * Read string from WASM memory
-   * @private
-   */
-  _readString(ptr) {
-    const memory = new Uint8Array(this.wasm.instance.exports.memory.buffer);
-    let end = ptr;
-    while (memory[end] !== 0) end++;
-    const bytes = memory.slice(ptr, end);
-    const decoder = new TextDecoder();
-    return decoder.decode(bytes);
-  }
-
-  /**
-   * Reset the WASM heap (call between conversions to reclaim memory)
-   */
-  resetHeap() {
-    if (this.ready) {
-      this.wasm.instance.exports.heap_reset();
-    }
-  }
-
-  /**
-   * Convert markdown string to HTML string using WASM
-   * @param {string} markdown - The markdown text to convert
-   * @returns {string} The converted HTML
-   */
-  convert(markdown) {
-    if (!this.ready) {
-      throw new Error('WASM module not initialized. Call init() first.');
-    }
-
-    // Reset heap before each conversion to reclaim memory
-    this.resetHeap();
-
-    // Write markdown to WASM memory
-    const inputPtr = this._writeString(markdown);
-
-    // Call the C function
-    const outputPtr = this.wasm.instance.exports.markdown_to_html(inputPtr);
-
-    // Read the result
-    const html = this._readString(outputPtr);
-
-    return html;
-  }
-
-  /**
-   * Convert markdown to DOM elements (compatible with original API)
-   * @param {string} markdown - The markdown text to convert
-   * @returns {Array<HTMLElement>} Array of DOM elements
-   */
-  convertToElements(markdown) {
-    const html = this.convert(markdown);
-    const template = document.createElement('template');
-    template.innerHTML = html;
-    return Array.from(template.content.children);
-  }
-}
-
-// Singleton instance
-const markdownWasm = new MarkdownConverterWasm();
-
-/**
- * Convert markdown to DOM elements (WASM version)
- * Compatible with original markdownConverter() API
- * @param {string} value - Markdown string
- * @returns {Promise<Array<HTMLElement>>} Array of DOM elements
- */
-async function markdownConverterWasm(value) {
-  if (!markdownWasm.ready) {
-    await markdownWasm.init();
-  }
-  return markdownWasm.convertToElements(value);
-}
-
-/**
- * Render markdown to a container element (WASM version)
- * Compatible with original renderMarkdown() API
- * @param {HTMLElement} container - Container element
- * @param {string} markdown - Markdown string
- */
-async function renderMarkdownWasm(container, markdown) {
-  if (!markdownWasm.ready) {
-    await markdownWasm.init();
-  }
-  container.innerHTML = markdownWasm.convert(markdown);
-}
-
-// Export for use in other files
-if (typeof module !== 'undefined' && module.exports) {
-  module.exports = {
-    MarkdownConverterWasm,
-    markdownWasm,
-    markdownConverterWasm,
-    renderMarkdownWasm
-  };
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/markdown_converter/tests/BUILD	Mon Jan 12 15:20:39 2026 -0800
@@ -0,0 +1,13 @@
+load("//gui_ze:gui_ze.bzl", "bun_run", "move_files_into_dir")
+
+# Test for WASM module (run with: bazel test //markdown_converter:markdown_to_html_wasm_test)
+filegroup(
+  name = "all_test_files",
+  srcs = ["//markdown_converter/wasm:markdown_to_html_wasm"],
+)
+
+bun_run(
+  name = "markdown_to_html_wasm_test",
+  src = "test_wasm.js",
+  data = [":all_test_files"],
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/markdown_converter/tests/test_wasm.js	Mon Jan 12 15:20:39 2026 -0800
@@ -0,0 +1,150 @@
+/**
+ * Test suite for markdown_to_html WASM module
+ */
+const fs = require('fs');
+// TODO: Fix the rules so that it can do relative import.
+const wasm = fs.readFileSync('markdown_converter/wasm/markdown_to_html_wasm.wasm');
+
+let testsPassed = 0;
+let testsFailed = 0;
+
+function assertEqual(actual, expected, testName) {
+  if (actual === expected) {
+    console.log(`✓ ${testName}`);
+    testsPassed++;
+  } else {
+    console.log(`✗ ${testName}`);
+    console.log(`  Expected: ${expected}`);
+    console.log(`  Actual:   ${actual}`);
+    testsFailed++;
+  }
+}
+
+function assertContains(actual, expected, testName) {
+  if (actual.includes(expected)) {
+    console.log(`✓ ${testName}`);
+    testsPassed++;
+  } else {
+    console.log(`✗ ${testName}`);
+    console.log(`  Expected to contain: ${expected}`);
+    console.log(`  Actual: ${actual}`);
+    testsFailed++;
+  }
+}
+
+async function runTests() {
+  const module = await WebAssembly.instantiate(wasm, { env: {} });
+  const { malloc, heap_reset, markdown_to_html, memory } = module.instance.exports;
+
+  function writeString(str) {
+    const encoder = new TextEncoder();
+    const bytes = encoder.encode(str + '\0');
+    const ptr = malloc(bytes.length);
+    new Uint8Array(memory.buffer).set(bytes, ptr);
+    return ptr;
+  }
+
+  function readString(ptr) {
+    const mem = new Uint8Array(memory.buffer);
+    let end = ptr;
+    while (mem[end] !== 0) end++;
+    return new TextDecoder().decode(mem.slice(ptr, end));
+  }
+
+  function convert(md) {
+    heap_reset();
+    const inputPtr = writeString(md);
+    const outputPtr = markdown_to_html(inputPtr);
+    return readString(outputPtr);
+  }
+
+  console.log('Running markdown_to_html WASM tests...\n');
+
+  // Test: Headings
+  console.log('--- Headings ---');
+  assertEqual(convert('# Hello'), '<h1>Hello</h1>', 'H1 heading');
+  assertEqual(convert('## World'), '<h2>World</h2>', 'H2 heading');
+  assertEqual(convert('### Level 3'), '<h3>Level 3</h3>', 'H3 heading');
+  assertEqual(convert('###### Level 6'), '<h6>Level 6</h6>', 'H6 heading');
+
+  // Test: Inline formatting
+  console.log('\n--- Inline Formatting ---');
+  assertEqual(convert('**bold**'), '<p><strong>bold</strong></p>', 'Bold with **');
+  assertEqual(convert('__bold__'), '<p><strong>bold</strong></p>', 'Bold with __');
+  assertEqual(convert('*italic*'), '<p><em>italic</em></p>', 'Italic with *');
+  assertEqual(convert('_italic_'), '<p><em>italic</em></p>', 'Italic with _');
+  assertEqual(convert('~~strike~~'), '<p><del>strike</del></p>', 'Strikethrough');
+  assertEqual(convert('`code`'), '<p><code>code</code></p>', 'Inline code');
+
+  // Test: Links and Images
+  console.log('\n--- Links and Images ---');
+  assertEqual(convert('[link](http://example.com)'), '<p><a href="http://example.com">link</a></p>', 'Link');
+  assertEqual(convert('![alt](http://img.png)'), '<p><img src="http://img.png" alt="alt"></p>', 'Image');
+
+  // Test: Lists
+  console.log('\n--- Lists ---');
+  assertEqual(convert('- item1\n- item2'), '<ul><li>item1</li><li>item2</li></ul>', 'Unordered list with -');
+  assertEqual(convert('* item1\n* item2'), '<ul><li>item1</li><li>item2</li></ul>', 'Unordered list with *');
+  assertEqual(convert('1. first\n2. second'), '<ol><li>first</li><li>second</li></ol>', 'Ordered list');
+
+  // Test: Code block
+  console.log('\n--- Code Block ---');
+  const codeBlock = '```\nfunction test() {\n  return 42;\n}\n```';
+  assertContains(convert(codeBlock), '<pre><code>', 'Code block opens');
+  assertContains(convert(codeBlock), '</code></pre>', 'Code block closes');
+
+  // Test: Blockquote
+  console.log('\n--- Blockquote ---');
+  assertEqual(convert('> quote'), '<blockquote>quote </blockquote>', 'Blockquote');
+
+  // Test: Horizontal rule
+  console.log('\n--- Horizontal Rule ---');
+  assertEqual(convert('---'), '<hr>', 'HR with ---');
+  assertEqual(convert('***'), '<hr>', 'HR with ***');
+  assertEqual(convert('___'), '<hr>', 'HR with ___');
+
+  // Test: Tables
+  console.log('\n--- Tables ---');
+  const table = '| Name | Age |\n|------|-----|\n| Alice | 30 |\n| Bob | 25 |';
+  const tableHtml = convert(table);
+  assertContains(tableHtml, '<table>', 'Table opens');
+  assertContains(tableHtml, '</table>', 'Table closes');
+  assertContains(tableHtml, '<thead>', 'Table has thead');
+  assertContains(tableHtml, '<tbody>', 'Table has tbody');
+  assertContains(tableHtml, '<th>Name</th>', 'Table header cell');
+  assertContains(tableHtml, '<td>Alice</td>', 'Table body cell');
+
+  // Test: Complex table with inline formatting
+  console.log('\n--- Table with Inline Formatting ---');
+  const complexTable = '| Feature | Status |\n|---------|--------|\n| **Bold** | *done* |';
+  const complexTableHtml = convert(complexTable);
+  assertContains(complexTableHtml, '<strong>Bold</strong>', 'Bold in table cell');
+  assertContains(complexTableHtml, '<em>done</em>', 'Italic in table cell');
+
+  // Test: HTML escaping
+  console.log('\n--- HTML Escaping ---');
+  assertContains(convert('<script>'), '&lt;script&gt;', 'Escapes < and >');
+  assertContains(convert('a & b'), '&amp;', 'Escapes &');
+
+  // Test: Mixed content
+  console.log('\n--- Mixed Content ---');
+  const mixed = '# Title\n\nSome **bold** text.\n\n- item 1\n- item 2';
+  const mixedHtml = convert(mixed);
+  assertContains(mixedHtml, '<h1>Title</h1>', 'Mixed: heading');
+  assertContains(mixedHtml, '<strong>bold</strong>', 'Mixed: bold');
+  assertContains(mixedHtml, '<ul>', 'Mixed: list');
+
+  // Summary
+  console.log('\n========================================');
+  console.log(`Tests passed: ${testsPassed}`);
+  console.log(`Tests failed: ${testsFailed}`);
+
+  if (testsFailed > 0) {
+    process.exit(1);
+  }
+}
+
+runTests().catch(err => {
+  console.error('Test error:', err);
+  process.exit(1);
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/markdown_converter/wasm/BUILD	Mon Jan 12 15:20:39 2026 -0800
@@ -0,0 +1,23 @@
+load("//gui_ze:gui_ze.bzl", "wasm_binary")
+
+wasm_binary(
+  name = "markdown_to_html_wasm",
+  src = "markdown_to_html_wasm.c",
+  exports = [
+    "malloc",
+    "free",
+    "heap_reset",
+    "markdown_to_html",
+    "markdown_strlen",
+  ],
+  visibility = ["//visibility:public"],
+)
+
+# JS to link wasm with 
+filegroup(
+  name = "markdown_to_html_wasm_js",
+  srcs = glob([
+      "**/*.js",
+  ], allow_empty=True),
+  visibility = ["//visibility:public"],
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/markdown_converter/wasm/markdown_to_html_wasm.c	Mon Jan 12 15:20:39 2026 -0800
@@ -0,0 +1,698 @@
+/**
+ * Markdown to HTML Converter - Standalone WASM Implementation
+ * No libc dependencies - can be compiled with: clang --target=wasm32
+ */
+
+#define WASM_EXPORT __attribute__((visibility("default")))
+
+typedef unsigned long size_t;
+typedef int int32_t;
+
+// Simple bump allocator for WASM
+#define HEAP_SIZE (1024 * 1024)  // 1MB heap
+static char heap[HEAP_SIZE];
+static size_t heap_offset = 0;
+
+WASM_EXPORT void *malloc(size_t size)
+{
+  // Align to 8 bytes
+  size_t aligned_offset = (heap_offset + 7) & ~7;
+  if (aligned_offset + size > HEAP_SIZE) return 0;
+
+  void *ptr = &heap[aligned_offset];
+  heap_offset = aligned_offset + size;
+  return ptr;
+}
+
+WASM_EXPORT void free(void *ptr)
+{
+  // Simple bump allocator - no actual free
+  (void)ptr;
+}
+
+WASM_EXPORT void heap_reset(void)
+{
+  heap_offset = 0;
+}
+
+// String functions
+static size_t strlen(const char *s)
+{
+  size_t len = 0;
+  while (s[len]) len++;
+  return len;
+}
+
+static void *memcpy(void *dest, const void *src, size_t n)
+{
+  char *d = (char *)dest;
+  const char *s = (const char *)src;
+  while (n--) *d++ = *s++;
+  return dest;
+}
+
+static int isspace_c(int c)
+{
+  return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v';
+}
+
+static int isdigit_c(int c)
+{
+  return c >= '0' && c <= '9';
+}
+
+// String buffer for building HTML output
+typedef struct {
+  char  *data;
+  size_t length;
+  size_t capacity;
+} StringBuffer;
+
+static StringBuffer *buffer_create(size_t initial_capacity)
+{
+  StringBuffer *buf = (StringBuffer *)malloc(sizeof(StringBuffer));
+  if (!buf) return 0;
+
+  buf->data = (char *)malloc(initial_capacity);
+  if (!buf->data) return 0;
+
+  buf->data[0] = '\0';
+  buf->length = 0;
+  buf->capacity = initial_capacity;
+  return buf;
+}
+
+static void buffer_grow(StringBuffer *buf, size_t needed)
+{
+  if (buf->length + needed + 1 > buf->capacity) {
+    size_t new_capacity = buf->capacity * 2;
+    while (new_capacity < buf->length + needed + 1)
+      new_capacity *= 2;
+
+    char *new_data = (char *)malloc(new_capacity);
+    if (new_data) {
+      memcpy(new_data, buf->data, buf->length + 1);
+      buf->data = new_data;
+      buf->capacity = new_capacity;
+    }
+  }
+}
+
+static void buffer_append(StringBuffer *buf, const char *str)
+{
+  size_t len = strlen(str);
+  buffer_grow(buf, len);
+  memcpy(buf->data + buf->length, str, len + 1);
+  buf->length += len;
+}
+
+static void buffer_append_n(StringBuffer *buf, const char *str, size_t n)
+{
+  buffer_grow(buf, n);
+  memcpy(buf->data + buf->length, str, n);
+  buf->length += n;
+  buf->data[buf->length] = '\0';
+}
+
+static void buffer_append_char(StringBuffer *buf, char c)
+{
+  buffer_grow(buf, 1);
+  buf->data[buf->length++] = c;
+  buf->data[buf->length] = '\0';
+}
+
+// Check if line starts with pattern (after trimming whitespace)
+static int starts_with(const char *line, const char *pattern)
+{
+  while (*line && isspace_c(*line)) line++;
+  size_t plen = strlen(pattern);
+  for (size_t i = 0; i < plen; i++) {
+    if (line[i] != pattern[i]) return 0;
+  }
+  return 1;
+}
+
+// Count leading # characters
+static int count_heading_level(const char *line)
+{
+  int count = 0;
+  while (*line && isspace_c(*line)) line++;
+  while (line[count] == '#' && count < 6) count++;
+  if (count > 0 && line[count] == ' ') return count;
+  return 0;
+}
+
+// Skip whitespace
+static const char *skip_whitespace(const char *str)
+{
+  while (*str && isspace_c(*str)) str++;
+  return str;
+}
+
+// Check if line is empty
+static int is_empty_line(const char *line)
+{
+  while (*line) {
+    if (!isspace_c(*line)) return 0;
+    line++;
+  }
+  return 1;
+}
+
+// Check if line is horizontal rule
+static int is_horizontal_rule(const char *line)
+{
+  line = skip_whitespace(line);
+  char first = *line;
+  if (first != '-' && first != '*' && first != '_') return 0;
+
+  int count = 0;
+  while (*line) {
+    if (*line == first) count++;
+    else if (!isspace_c(*line)) return 0;
+    line++;
+  }
+  return count >= 3;
+}
+
+// Check if line is unordered list item
+static int is_unordered_list(const char *line)
+{
+  line = skip_whitespace(line);
+  return (*line == '-' || *line == '*' || *line == '+') && line[1] == ' ';
+}
+
+// Check if line is ordered list item
+static int is_ordered_list(const char *line)
+{
+  line = skip_whitespace(line);
+  while (*line && isdigit_c(*line)) line++;
+  return *line == '.' && line[1] == ' ';
+}
+
+// Check if line is a table row (starts with |)
+static int is_table_row(const char *line)
+{
+  line = skip_whitespace(line);
+  return *line == '|';
+}
+
+// Check if line is a table separator row (| --- | --- |)
+static int is_table_separator(const char *line)
+{
+  line = skip_whitespace(line);
+  if (*line != '|') return 0;
+  line++;
+
+  int has_dash = 0;
+  while (*line) {
+    if (*line == '-') has_dash = 1;
+    else if (*line == '|' || *line == ':' || isspace_c(*line)) { /* ok */ }
+    else return 0;
+    line++;
+  }
+  return has_dash;
+}
+
+// Forward declaration for process_inline
+static void process_inline(StringBuffer *buf, const char *text, size_t len);
+
+// Parse table cells from a row and append to buffer
+static void parse_table_row(StringBuffer *buf, const char *line, int is_header)
+{
+  const char *cell_tag = is_header ? "th" : "td";
+
+  buffer_append(buf, "<tr>");
+
+  line = skip_whitespace(line);
+  if (*line == '|') line++; // Skip leading |
+
+  while (*line) {
+    // Skip whitespace before cell content
+    while (*line && isspace_c(*line)) line++;
+
+    // Find cell end (next | or end of line)
+    const char *cell_start = line;
+    while (*line && *line != '|') line++;
+
+    // Trim trailing whitespace from cell
+    const char *cell_end = line;
+    while (cell_end > cell_start && isspace_c(*(cell_end - 1))) cell_end--;
+
+    size_t cell_len = cell_end - cell_start;
+
+    // Only output cell if we have content or more cells coming
+    if (cell_len > 0 || *line == '|') {
+      buffer_append(buf, "<");
+      buffer_append(buf, cell_tag);
+      buffer_append(buf, ">");
+      if (cell_len > 0) {
+        process_inline(buf, cell_start, cell_len);
+      }
+      buffer_append(buf, "</");
+      buffer_append(buf, cell_tag);
+      buffer_append(buf, ">");
+    }
+
+    if (*line == '|') line++; // Skip |
+
+    // Check if this was the trailing |
+    const char *rest = line;
+    while (*rest && isspace_c(*rest)) rest++;
+    if (!*rest) break; // End of line after trailing |
+  }
+
+  buffer_append(buf, "</tr>");
+}
+
+// Process inline markdown
+static void process_inline(StringBuffer *buf, const char *text, size_t len)
+{
+  size_t i = 0;
+
+  while (i < len) {
+    // Links: [text](url)
+    if (text[i] == '[') {
+      size_t link_start = i + 1;
+      size_t link_end = link_start;
+      while (link_end < len && text[link_end] != ']') link_end++;
+
+      if (link_end < len && link_end + 1 < len && text[link_end + 1] == '(') {
+        size_t url_start = link_end + 2;
+        size_t url_end = url_start;
+        while (url_end < len && text[url_end] != ')') url_end++;
+
+        if (url_end < len) {
+          buffer_append(buf, "<a href=\"");
+          buffer_append_n(buf, text + url_start, url_end - url_start);
+          buffer_append(buf, "\">");
+          buffer_append_n(buf, text + link_start, link_end - link_start);
+          buffer_append(buf, "</a>");
+          i = url_end + 1;
+          continue;
+        }
+      }
+    }
+
+    // Images: ![alt](url)
+    if (text[i] == '!' && i + 1 < len && text[i + 1] == '[') {
+      size_t alt_start = i + 2;
+      size_t alt_end = alt_start;
+      while (alt_end < len && text[alt_end] != ']') alt_end++;
+
+      if (alt_end < len && alt_end + 1 < len && text[alt_end + 1] == '(') {
+        size_t url_start = alt_end + 2;
+        size_t url_end = url_start;
+        while (url_end < len && text[url_end] != ')') url_end++;
+
+        if (url_end < len) {
+          buffer_append(buf, "<img src=\"");
+          buffer_append_n(buf, text + url_start, url_end - url_start);
+          buffer_append(buf, "\" alt=\"");
+          buffer_append_n(buf, text + alt_start, alt_end - alt_start);
+          buffer_append(buf, "\">");
+          i = url_end + 1;
+          continue;
+        }
+      }
+    }
+
+    // Bold: **text** or __text__
+    if ((text[i] == '*' && i + 1 < len && text[i + 1] == '*') ||
+        (text[i] == '_' && i + 1 < len && text[i + 1] == '_')) {
+      char marker = text[i];
+      size_t start = i + 2;
+      size_t end = start;
+      while (end + 1 < len && !(text[end] == marker && text[end + 1] == marker)) end++;
+
+      if (end + 1 < len) {
+        buffer_append(buf, "<strong>");
+        process_inline(buf, text + start, end - start);
+        buffer_append(buf, "</strong>");
+        i = end + 2;
+        continue;
+      }
+    }
+
+    // Strikethrough: ~~text~~
+    if (text[i] == '~' && i + 1 < len && text[i + 1] == '~') {
+      size_t start = i + 2;
+      size_t end = start;
+      while (end + 1 < len && !(text[end] == '~' && text[end + 1] == '~')) end++;
+
+      if (end + 1 < len) {
+        buffer_append(buf, "<del>");
+        process_inline(buf, text + start, end - start);
+        buffer_append(buf, "</del>");
+        i = end + 2;
+        continue;
+      }
+    }
+
+    // Italic: *text* or _text_
+    if ((text[i] == '*' || text[i] == '_') && i + 1 < len && !isspace_c(text[i + 1])) {
+      char marker = text[i];
+      size_t start = i + 1;
+      size_t end = start;
+      while (end < len && text[end] != marker) end++;
+
+      if (end < len && end > start) {
+        buffer_append(buf, "<em>");
+        process_inline(buf, text + start, end - start);
+        buffer_append(buf, "</em>");
+        i = end + 1;
+        continue;
+      }
+    }
+
+    // Inline code: `code`
+    if (text[i] == '`') {
+      size_t start = i + 1;
+      size_t end = start;
+      while (end < len && text[end] != '`') end++;
+
+      if (end < len) {
+        buffer_append(buf, "<code>");
+        buffer_append_n(buf, text + start, end - start);
+        buffer_append(buf, "</code>");
+        i = end + 1;
+        continue;
+      }
+    }
+
+    // HTML escape
+    if (text[i] == '<') {
+      buffer_append(buf, "&lt;");
+    } else if (text[i] == '>') {
+      buffer_append(buf, "&gt;");
+    } else if (text[i] == '&') {
+      buffer_append(buf, "&amp;");
+    } else {
+      buffer_append_char(buf, text[i]);
+    }
+    i++;
+  }
+}
+
+// Append heading tag
+static void append_heading_tag(StringBuffer *buf, int level, int closing)
+{
+  buffer_append_char(buf, '<');
+  if (closing) buffer_append_char(buf, '/');
+  buffer_append_char(buf, 'h');
+  buffer_append_char(buf, '0' + level);
+  buffer_append_char(buf, '>');
+}
+
+// Convert markdown to HTML
+WASM_EXPORT char *markdown_to_html(const char *markdown)
+{
+  if (!markdown) return 0;
+
+  StringBuffer *buf = buffer_create(4096);
+  if (!buf) return 0;
+
+  const char *ptr = markdown;
+  const char *line_start;
+
+  while (*ptr) {
+    line_start = ptr;
+
+    // Find end of line
+    while (*ptr && *ptr != '\n') ptr++;
+    size_t line_len = ptr - line_start;
+
+    // Create line copy
+    char *line = (char *)malloc(line_len + 1);
+    if (!line) return buf->data;
+    memcpy(line, line_start, line_len);
+    line[line_len] = '\0';
+
+    // Skip empty lines
+    if (is_empty_line(line)) {
+      if (*ptr == '\n') ptr++;
+      continue;
+    }
+
+    // Headings
+    int heading_level = count_heading_level(line);
+    if (heading_level > 0) {
+      const char *content = skip_whitespace(line);
+      while (*content == '#') content++;
+      content = skip_whitespace(content);
+
+      append_heading_tag(buf, heading_level, 0);
+      process_inline(buf, content, strlen(content));
+      append_heading_tag(buf, heading_level, 1);
+
+      if (*ptr == '\n') ptr++;
+      continue;
+    }
+
+    // Code block
+    if (starts_with(line, "```")) {
+      buffer_append(buf, "<pre><code>");
+      if (*ptr == '\n') ptr++;
+
+      while (*ptr) {
+        line_start = ptr;
+        while (*ptr && *ptr != '\n') ptr++;
+        line_len = ptr - line_start;
+
+        char *code_line = (char *)malloc(line_len + 1);
+        if (!code_line) break;
+        memcpy(code_line, line_start, line_len);
+        code_line[line_len] = '\0';
+
+        if (starts_with(code_line, "```")) {
+          if (*ptr == '\n') ptr++;
+          break;
+        }
+
+        for (size_t i = 0; i < line_len; i++) {
+          if (code_line[i] == '<') buffer_append(buf, "&lt;");
+          else if (code_line[i] == '>') buffer_append(buf, "&gt;");
+          else if (code_line[i] == '&') buffer_append(buf, "&amp;");
+          else buffer_append_char(buf, code_line[i]);
+        }
+        buffer_append_char(buf, '\n');
+
+        if (*ptr == '\n') ptr++;
+      }
+
+      buffer_append(buf, "</code></pre>");
+      continue;
+    }
+
+    // Blockquote
+    if (starts_with(line, ">")) {
+      buffer_append(buf, "<blockquote>");
+
+      while (1) {
+        const char *content = skip_whitespace(line);
+        if (*content == '>') content++;
+        content = skip_whitespace(content);
+        process_inline(buf, content, strlen(content));
+        buffer_append_char(buf, ' ');
+
+        if (*ptr == '\n') ptr++;
+        if (!*ptr) break;
+
+        line_start = ptr;
+        while (*ptr && *ptr != '\n') ptr++;
+        line_len = ptr - line_start;
+
+        line = (char *)malloc(line_len + 1);
+        if (!line) break;
+        memcpy(line, line_start, line_len);
+        line[line_len] = '\0';
+
+        if (!starts_with(line, ">")) {
+          ptr = line_start;
+          break;
+        }
+      }
+
+      buffer_append(buf, "</blockquote>");
+      continue;
+    }
+
+    // Horizontal rule
+    if (is_horizontal_rule(line)) {
+      buffer_append(buf, "<hr>");
+      if (*ptr == '\n') ptr++;
+      continue;
+    }
+
+    // Unordered list
+    if (is_unordered_list(line)) {
+      buffer_append(buf, "<ul>");
+
+      while (1) {
+        const char *content = skip_whitespace(line);
+        content += 2;
+
+        buffer_append(buf, "<li>");
+        process_inline(buf, content, strlen(content));
+        buffer_append(buf, "</li>");
+
+        if (*ptr == '\n') ptr++;
+        if (!*ptr) break;
+
+        line_start = ptr;
+        while (*ptr && *ptr != '\n') ptr++;
+        line_len = ptr - line_start;
+
+        line = (char *)malloc(line_len + 1);
+        if (!line) break;
+        memcpy(line, line_start, line_len);
+        line[line_len] = '\0';
+
+        if (!is_unordered_list(line)) {
+          ptr = line_start;
+          break;
+        }
+      }
+
+      buffer_append(buf, "</ul>");
+      continue;
+    }
+
+    // Ordered list
+    if (is_ordered_list(line)) {
+      buffer_append(buf, "<ol>");
+
+      while (1) {
+        const char *content = skip_whitespace(line);
+        while (*content && isdigit_c(*content)) content++;
+        if (*content == '.') content++;
+        content = skip_whitespace(content);
+
+        buffer_append(buf, "<li>");
+        process_inline(buf, content, strlen(content));
+        buffer_append(buf, "</li>");
+
+        if (*ptr == '\n') ptr++;
+        if (!*ptr) break;
+
+        line_start = ptr;
+        while (*ptr && *ptr != '\n') ptr++;
+        line_len = ptr - line_start;
+
+        line = (char *)malloc(line_len + 1);
+        if (!line) break;
+        memcpy(line, line_start, line_len);
+        line[line_len] = '\0';
+
+        if (!is_ordered_list(line)) {
+          ptr = line_start;
+          break;
+        }
+      }
+
+      buffer_append(buf, "</ol>");
+      continue;
+    }
+
+    // Table
+    if (is_table_row(line)) {
+      // Check if next line is a separator (to confirm this is a table)
+      const char *peek_ptr = ptr;
+      if (*peek_ptr == '\n') peek_ptr++;
+
+      const char *next_line_start = peek_ptr;
+      while (*peek_ptr && *peek_ptr != '\n') peek_ptr++;
+      size_t next_line_len = peek_ptr - next_line_start;
+
+      char *next_line = (char *)malloc(next_line_len + 1);
+      if (next_line) {
+        memcpy(next_line, next_line_start, next_line_len);
+        next_line[next_line_len] = '\0';
+
+        if (is_table_separator(next_line)) {
+          // It's a valid table
+          buffer_append(buf, "<table>");
+
+          // Header row
+          buffer_append(buf, "<thead>");
+          parse_table_row(buf, line, 1);
+          buffer_append(buf, "</thead>");
+
+          // Skip to after separator
+          if (*ptr == '\n') ptr++;
+          ptr = peek_ptr;
+          if (*ptr == '\n') ptr++;
+
+          // Body rows
+          buffer_append(buf, "<tbody>");
+          while (*ptr) {
+            line_start = ptr;
+            while (*ptr && *ptr != '\n') ptr++;
+            line_len = ptr - line_start;
+
+            line = (char *)malloc(line_len + 1);
+            if (!line) break;
+            memcpy(line, line_start, line_len);
+            line[line_len] = '\0';
+
+            if (!is_table_row(line) || is_empty_line(line)) {
+              ptr = line_start;
+              break;
+            }
+
+            parse_table_row(buf, line, 0);
+            if (*ptr == '\n') ptr++;
+          }
+          buffer_append(buf, "</tbody>");
+
+          buffer_append(buf, "</table>");
+          continue;
+        }
+      }
+    }
+
+    // Paragraph
+    buffer_append(buf, "<p>");
+
+    while (1) {
+      const char *content = skip_whitespace(line);
+      process_inline(buf, content, strlen(content));
+
+      if (*ptr == '\n') ptr++;
+      if (!*ptr) break;
+
+      line_start = ptr;
+      while (*ptr && *ptr != '\n') ptr++;
+      line_len = ptr - line_start;
+
+      line = (char *)malloc(line_len + 1);
+      if (!line) break;
+      memcpy(line, line_start, line_len);
+      line[line_len] = '\0';
+
+      if (is_empty_line(line) ||
+          count_heading_level(line) > 0 ||
+          starts_with(line, "```") ||
+          starts_with(line, ">") ||
+          is_horizontal_rule(line) ||
+          is_unordered_list(line) ||
+          is_ordered_list(line) ||
+          is_table_row(line)) {
+        ptr = line_start;
+        break;
+      }
+
+      buffer_append_char(buf, ' ');
+    }
+
+    buffer_append(buf, "</p>");
+  }
+
+  return buf->data;
+}
+
+// Get string length (for JS interop)
+WASM_EXPORT size_t markdown_strlen(const char *str)
+{
+  return str ? strlen(str) : 0;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/markdown_converter/wasm/markdown_to_html_wasm.js	Mon Jan 12 15:20:39 2026 -0800
@@ -0,0 +1,143 @@
+/**
+ * Markdown to HTML Converter - WASM FFI Wrapper
+ * Loads the C implementation via WebAssembly and provides JavaScript API
+ *
+ * Build the WASM with: bazel build //markdown_converter:markdown_to_html_wasm
+ */
+
+class MarkdownConverterWasm {
+  constructor() {
+    this.wasm = undefined;
+    this.ready = false;
+  }
+
+  /**
+   * Initialize the WASM module
+   * @param {string} wasmPath - Path to the .wasm file
+   * @returns {Promise<void>}
+   */
+  async init(wasmPath = '/markdown_to_html_wasm.wasm') {
+    if (this.ready) return;
+
+    try {
+      this.wasm = await WebAssembly.instantiateStreaming(fetch(wasmPath), {
+        env: {}
+      });
+      this.ready = true;
+    } catch (err) {
+      console.error('Failed to load markdown WASM module:', err);
+      throw err;
+    }
+  }
+
+  /**
+   * Write string to WASM memory
+   * @private
+   */
+  _writeString(str) {
+    const encoder = new TextEncoder();
+    const bytes = encoder.encode(str + '\0');
+    const ptr = this.wasm.instance.exports.malloc(bytes.length);
+    const memory = new Uint8Array(this.wasm.instance.exports.memory.buffer);
+    memory.set(bytes, ptr);
+    return ptr;
+  }
+
+  /**
+   * Read string from WASM memory
+   * @private
+   */
+  _readString(ptr) {
+    const memory = new Uint8Array(this.wasm.instance.exports.memory.buffer);
+    let end = ptr;
+    while (memory[end] !== 0) end++;
+    const bytes = memory.slice(ptr, end);
+    const decoder = new TextDecoder();
+    return decoder.decode(bytes);
+  }
+
+  /**
+   * Reset the WASM heap (call between conversions to reclaim memory)
+   */
+  resetHeap() {
+    if (this.ready) {
+      this.wasm.instance.exports.heap_reset();
+    }
+  }
+
+  /**
+   * Convert markdown string to HTML string using WASM
+   * @param {string} markdown - The markdown text to convert
+   * @returns {string} The converted HTML
+   */
+  convert(markdown) {
+    if (!this.ready) {
+      throw new Error('WASM module not initialized. Call init() first.');
+    }
+
+    // Reset heap before each conversion to reclaim memory
+    this.resetHeap();
+
+    // Write markdown to WASM memory
+    const inputPtr = this._writeString(markdown);
+
+    // Call the C function
+    const outputPtr = this.wasm.instance.exports.markdown_to_html(inputPtr);
+
+    // Read the result
+    const html = this._readString(outputPtr);
+
+    return html;
+  }
+
+  /**
+   * Convert markdown to DOM elements (compatible with original API)
+   * @param {string} markdown - The markdown text to convert
+   * @returns {Array<HTMLElement>} Array of DOM elements
+   */
+  convertToElements(markdown) {
+    const html = this.convert(markdown);
+    const template = document.createElement('template');
+    template.innerHTML = html;
+    return Array.from(template.content.children);
+  }
+}
+
+// Singleton instance
+const markdownWasm = new MarkdownConverterWasm();
+
+/**
+ * Convert markdown to DOM elements (WASM version)
+ * Compatible with original markdownConverter() API
+ * @param {string} value - Markdown string
+ * @returns {Promise<Array<HTMLElement>>} Array of DOM elements
+ */
+async function markdownConverterWasm(value) {
+  if (!markdownWasm.ready) {
+    await markdownWasm.init();
+  }
+  return markdownWasm.convertToElements(value);
+}
+
+/**
+ * Render markdown to a container element (WASM version)
+ * Compatible with original renderMarkdown() API
+ * @param {HTMLElement} container - Container element
+ * @param {string} markdown - Markdown string
+ */
+async function renderMarkdownWasm(container, markdown) {
+  if (!markdownWasm.ready) {
+    await markdownWasm.init();
+  }
+  container.innerHTML = markdownWasm.convert(markdown);
+}
+
+// Export for use in other files
+if (typeof module !== 'undefined' && module.exports) {
+  module.exports = {
+    MarkdownConverterWasm,
+    markdownWasm,
+    markdownConverterWasm,
+    renderMarkdownWasm
+  };
+}