Mercurial
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:  + * - 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:  - * - 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:  - 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, "<"); - } else if (text[i] == '>') { - buffer_append(buf, ">"); - } else if (text[i] == '&') { - buffer_append(buf, "&"); - } 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, "<"); - else if (code_line[i] == '>') buffer_append(buf, ">"); - else if (code_line[i] == '&') buffer_append(buf, "&"); - 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(''), '<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>'), '<script>', 'Escapes < and >'); + assertContains(convert('a & b'), '&', '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:  + 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, "<"); + } else if (text[i] == '>') { + buffer_append(buf, ">"); + } else if (text[i] == '&') { + buffer_append(buf, "&"); + } 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, "<"); + else if (code_line[i] == '>') buffer_append(buf, ">"); + else if (code_line[i] == '&') buffer_append(buf, "&"); + 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 + }; +}