///|
/// Hono JSX - Server-side JSX for Hono
/// https://hono.dev/docs/guides/jsx
///|
/// JSX Node type
#external
pub type JSXNode
///|
pub fn JSXNode::as_any(self : JSXNode) -> @core.Any = "%identity"
///|
/// HonoNode trait for JSX children
pub trait HonoNode {
fn toHonoNode(Self) -> @core.Any
}
///|
pub impl HonoNode for JSXNode with fn toHonoNode(self) {
self.as_any()
}
///|
pub impl HonoNode for String with fn toHonoNode(self) {
@core.any(self)
}
///|
pub impl HonoNode for Int with fn toHonoNode(self) {
@core.any(self)
}
///|
pub impl HonoNode for Double with fn toHonoNode(self) {
@core.any(self)
}
///|
pub impl HonoNode for Bool with fn toHonoNode(self) {
@core.any(self)
}
///|
pub impl[T : HonoNode] HonoNode for Array[T] with fn toHonoNode(self) {
@core.any(self.map(HonoNode::toHonoNode))
}
///|
/// Component function type
pub type FC[T] = (T) -> JSXNode
///| HonoJsx - initialized JSX runtime instance
///|
#external
pub type HonoJsx
///|
extern "js" fn ffi_import_hono_jsx_runtime() -> @js.Promise[@core.Any] =
#|() => import("hono/jsx/jsx-runtime")
///|
extern "js" fn ffi_import_hono_jsx() -> @js.Promise[@core.Any] =
#|() => import("hono/jsx")
///|
extern "js" fn ffi_import_hono_jsx_streaming() -> @js.Promise[@core.Any] =
#|() => import("hono/jsx/streaming")
///|
extern "js" fn ffi_import_hono_html() -> @js.Promise[@core.Any] =
#|() => import("hono/html")
///|
/// Import hono/jsx/jsx-runtime module
pub async fn import_hono_jsx() -> HonoJsx {
// Import all needed modules
let runtime = ffi_import_hono_jsx_runtime().wait()
let jsx_module = ffi_import_hono_jsx().wait()
let streaming = ffi_import_hono_jsx_streaming().wait()
let html_module = ffi_import_hono_html().wait()
// Store in a wrapper object
let obj = @core.new_object()
obj["jsxs"] = runtime._get("jsxs")
obj["Fragment"] = runtime._get("Fragment")
obj["memo"] = jsx_module._get("memo")
obj["createContext"] = jsx_module._get("createContext")
obj["useContext"] = jsx_module._get("useContext")
obj["renderToReadableStream"] = streaming._get("renderToReadableStream")
obj["raw"] = html_module._get("raw")
obj["html"] = html_module._get("html")
obj.cast()
}
///| FFI for HonoJsx methods
///|
extern "js" fn ffi_hono_jsx_jsxs(
m : HonoJsx,
tag : @core.Any,
props : @core.Any,
) -> JSXNode =
#|(m, tag, props) => m.jsxs(tag, props)
///|
extern "js" fn ffi_hono_jsx_fragment(m : HonoJsx) -> @core.Any =
#|(m) => m.Fragment
///|
extern "js" fn ffi_hono_jsx_memo(
m : HonoJsx,
component : @core.Any,
) -> @core.Any =
#|(m, component) => m.memo(component)
///|
extern "js" fn ffi_hono_jsx_create_context(
m : HonoJsx,
defaultValue : @core.Any,
) -> @core.Any =
#|(m, defaultValue) => m.createContext(defaultValue)
///|
extern "js" fn ffi_hono_jsx_use_context(
m : HonoJsx,
ctx : @core.Any,
) -> @core.Any =
#|(m, ctx) => m.useContext(ctx)
///|
extern "js" fn ffi_hono_jsx_render_to_readable_stream(
m : HonoJsx,
node : @core.Any,
) -> @core.Any =
#|(m, node) => m.renderToReadableStream(node)
///|
extern "js" fn ffi_hono_jsx_raw(m : HonoJsx, html : String) -> JSXNode =
#|(m, html) => m.raw(html)
///|
extern "js" fn ffi_hono_jsx_html(
m : HonoJsx,
template : FixedArray[String],
) -> HtmlEscapedString =
#|(m, template) => m.html(template)
///| HonoJsx methods
///|
/// Create a JSX element using HonoJsx instance
pub fn HonoJsx::jsx(
self : HonoJsx,
tag : String,
children : Array[&HonoNode],
props? : Map[String, @core.Any],
style? : Map[String, String],
class? : &CssClassName,
id? : String,
key? : String,
) -> JSXNode {
let tag = @core.any(tag)
let children_arr = children.map(child => child.toHonoNode())
let val = @core.new_object()
if props is Some(props) {
for k, v in props {
val[k] = v
}
}
if style is Some(style) {
let style_obj = @core.new_object()
for k, v in style {
style_obj[k] = @core.any(v)
}
val["style"] = style_obj
}
if class is Some(c) {
val["class"] = c.value()
}
if id is Some(i) {
val["id"] = @core.any(i)
}
if key is Some(k) {
val["key"] = @core.any(k)
}
// Flatten children
let flat_children : @core.Any = if children_arr.length() == 1 {
children_arr[0]
} else {
@core.any(children_arr)
}
val["children"] = flat_children
ffi_hono_jsx_jsxs(self, tag, val)
}
///|
/// Alias for jsx
pub fn HonoJsx::h(
self : HonoJsx,
tag : String,
children : Array[&HonoNode],
props? : Map[String, @core.Any],
style? : Map[String, String],
class? : &CssClassName,
id? : String,
key? : String,
) -> JSXNode {
self.jsx(tag, children, props?, style?, class?, id?, key?)
}
///|
/// Create element with component function
pub fn[T] HonoJsx::jsx_component(
self : HonoJsx,
tag : FC[T],
props : T,
children? : Array[&HonoNode] = [],
key? : String,
) -> JSXNode {
let tag : @core.Any = @core.any(tag)
let children_any = children.map(child => child.toHonoNode())
let val : @core.Any = @core.any(props)
if key is Some(k) {
if @core.is_nullish(val) {
let obj = @core.new_object()
obj["key"] = @core.any(k)
let flat_children : @core.Any = if children_any.length() == 1 {
children_any[0]
} else {
@core.any(children_any)
}
obj["children"] = flat_children
return ffi_hono_jsx_jsxs(self, tag, obj)
}
val["key"] = @core.any(k)
}
let flat_children : @core.Any = if children_any.length() == 1 {
children_any[0]
} else {
@core.any(children_any)
}
val["children"] = flat_children
ffi_hono_jsx_jsxs(self, tag, val)
}
///|
/// Fragment - groups elements without extra DOM nodes
pub fn HonoJsx::fragment(
self : HonoJsx,
children : Array[&HonoNode],
key? : String,
) -> JSXNode {
let children_arr = children.map(child => child.toHonoNode())
let val = @core.new_object()
if key is Some(k) {
val["key"] = @core.any(k)
}
let flat_children : @core.Any = if children_arr.length() == 1 {
children_arr[0]
} else {
@core.any(children_arr)
}
val["children"] = flat_children
ffi_hono_jsx_jsxs(self, ffi_hono_jsx_fragment(self), val)
}
///|
/// Render JSX to string
pub fn render_to_string(node : JSXNode) -> String {
ffi_render_to_string(node.as_any())
}
///|
extern "js" fn ffi_render_to_string(node : @core.Any) -> String =
#|(node) => {
#| if (typeof node === 'string') return node;
#| return node.toString();
#|}
///|
/// Render JSX to ReadableStream (streaming)
pub fn HonoJsx::render_to_readable_stream(
self : HonoJsx,
node : JSXNode,
) -> @core.Any {
ffi_hono_jsx_render_to_readable_stream(self, node.as_any())
}
///|
/// Memo - memoize a component
pub fn[T] HonoJsx::memo(self : HonoJsx, component : FC[T]) -> FC[T] {
ffi_hono_jsx_memo(self, @core.any(component)).cast()
}
///|
/// Context type
#external
pub type HonoContext[T]
///|
pub fn[T] HonoContext::as_any(self : HonoContext[T]) -> @core.Any = "%identity"
///|
/// Create a context
pub fn[T] HonoJsx::create_context(
self : HonoJsx,
defaultValue : T?,
) -> HonoContext[T] {
ffi_hono_jsx_create_context(self, @core.any(defaultValue)).cast()
}
///|
/// Use context value
pub fn[T] HonoJsx::use_context(self : HonoJsx, ctx : HonoContext[T]) -> T {
ffi_hono_jsx_use_context(self, ctx.as_any()).cast()
}
///|
/// Context Provider
pub fn[T] HonoJsx::context_provider(
self : HonoJsx,
ctx : HonoContext[T],
children : Array[&HonoNode],
value~ : T,
) -> JSXNode {
let children_any = children.map(child => child.toHonoNode())
let props = @core.new_object()
props["value"] = @core.any(value)
let flat_children : @core.Any = if children_any.length() == 1 {
children_any[0]
} else {
@core.any(children_any)
}
props["children"] = flat_children
ffi_hono_jsx_jsxs(self, ffi_get_provider(ctx.as_any()), props)
}
///|
extern "js" fn ffi_get_provider(ctx : @core.Any) -> @core.Any =
#|(ctx) => ctx.Provider
///|
/// Raw HTML - insert unescaped HTML
pub fn HonoJsx::raw(self : HonoJsx, html : String) -> JSXNode {
ffi_hono_jsx_raw(self, html)
}
///|
/// HTML tagged template helper result
#external
pub type HtmlEscapedString
///|
pub fn HtmlEscapedString::as_any(self : HtmlEscapedString) -> @core.Any = "%identity"
///|
pub impl HonoNode for HtmlEscapedString with fn toHonoNode(self) {
self.as_any()
}
///|
/// Create HTML from template string (escaped)
pub fn HonoJsx::html(self : HonoJsx, template : String) -> HtmlEscapedString {
ffi_hono_jsx_html(self, [template])
}
///|
/// Extend Context with JSX html method
pub fn[Env, ExecutionContext] Context::html_jsx(
self : Context[Env, ExecutionContext],
node : JSXNode,
status? : Int,
) -> @http.Response {
let html_string = render_to_string(node)
let status_code = match status {
Some(s) => s
None => 200
}
self
.as_any()
._call("html", [@core.any(html_string), @core.any(status_code)])
.cast()
}
///|
/// Extend Context with streaming JSX response
pub async fn[Env, ExecutionContext] Context::stream_jsx(
self : Context[Env, ExecutionContext],
hono_jsx : HonoJsx,
node : JSXNode,
status? : Int,
) -> @http.Response {
let stream = hono_jsx.render_to_readable_stream(node)
let status_code = match status {
Some(s) => s
None => 200
}
ffi_stream_response(self.as_any(), stream, status_code).wait()
}
///|
extern "js" fn ffi_stream_response(
ctx : @core.Any,
stream : @core.Any,
status : Int,
) -> @core.Promise[@http.Response] =
#|async (ctx, stream, status) => {
#| ctx.status(status);
#| ctx.header('Content-Type', 'text/html; charset=UTF-8');
#| ctx.header('Transfer-Encoding', 'chunked');
#| return new Response(stream);
#|}
///| Legacy API - now uses #module for ESM compatibility
///|
///| TODO: Fragment is a value (not a function), so #module cannot be used.
///| Currently using globalThis initialization pattern. Consider:
///| - Using HonoJsx API instead (recommended for new code)
///| - Finding a better pattern for value exports in MoonBit FFI
///|
/// jsxs function from hono/jsx/jsx-runtime (ESM)
#module("hono/jsx/jsx-runtime")
extern "js" fn ffi_jsxs_esm(tag : @core.Any, props : @core.Any) -> JSXNode = "jsxs"
///|
/// Get Fragment from global (initialized by init_jsx_runtime)
extern "js" fn ffi_get_fragment_global() -> @core.Any =
#|() => globalThis.__HonoJsxFragment
///|
/// Import and store hono/jsx/jsx-runtime globally
extern "js" fn ffi_init_jsx_runtime() -> @js.Promise[Unit] =
#|() => import('hono/jsx/jsx-runtime').then(m => { globalThis.__HonoJsxFragment = m.Fragment; })
///|
/// Initialize the JSX runtime (must be called before using legacy jsx/fragment)
/// This must be called before using the legacy `fragment` function in ESM environments.
pub async fn init_jsx_runtime() -> Unit {
ffi_init_jsx_runtime().wait()
}
///|
/// Create a JSX element (legacy - prefer HonoJsx::jsx)
/// class parameter accepts String or CssClass via CssClassName trait
#alias(h)
pub fn jsx(
tag : String,
children : Array[&HonoNode],
props? : Map[String, @core.Any],
style? : Map[String, String],
class? : &CssClassName,
id? : String,
key? : String,
) -> JSXNode {
let tag = @core.any(tag)
let children_arr = children.map(child => child.toHonoNode())
let val = @core.new_object()
if props is Some(props) {
for k, v in props {
val[k] = v
}
}
if style is Some(style) {
let style_obj = @core.new_object()
for k, v in style {
style_obj[k] = @core.any(v)
}
val["style"] = style_obj
}
if class is Some(c) {
val["class"] = c.value()
}
if id is Some(i) {
val["id"] = @core.any(i)
}
if key is Some(k) {
val["key"] = @core.any(k)
}
// Flatten children
let flat_children : @core.Any = if children_arr.length() == 1 {
children_arr[0]
} else {
@core.any(children_arr)
}
val["children"] = flat_children
ffi_jsxs_esm(tag, val)
}
///|
/// Create element with component function (legacy)
pub fn[T] jsx_component(
tag : FC[T],
props : T,
children? : Array[&HonoNode] = [],
key? : String,
) -> JSXNode {
let tag : @core.Any = @core.any(tag)
let children_any = children.map(child => child.toHonoNode())
let val : @core.Any = @core.any(props)
if key is Some(k) {
if @core.is_nullish(val) {
let obj = @core.new_object()
obj["key"] = @core.any(k)
let flat_children : @core.Any = if children_any.length() == 1 {
children_any[0]
} else {
@core.any(children_any)
}
obj["children"] = flat_children
return ffi_jsxs_esm(tag, obj)
}
val["key"] = @core.any(k)
}
let flat_children : @core.Any = if children_any.length() == 1 {
children_any[0]
} else {
@core.any(children_any)
}
val["children"] = flat_children
ffi_jsxs_esm(tag, val)
}
///|
/// Fragment (legacy - prefer HonoJsx::fragment)
/// Note: Requires init_jsx_runtime() to be called first
pub fn fragment(children : Array[&HonoNode], key? : String) -> JSXNode {
let children_arr = children.map(child => child.toHonoNode())
let val = @core.new_object()
if key is Some(k) {
val["key"] = @core.any(k)
}
let flat_children : @core.Any = if children_arr.length() == 1 {
children_arr[0]
} else {
@core.any(children_arr)
}
val["children"] = flat_children
ffi_jsxs_esm(ffi_get_fragment_global(), val)
}
///|
/// Raw HTML (legacy - prefer HonoJsx::raw)
#module("hono/html")
extern "js" fn ffi_raw_esm(html : String) -> JSXNode = "raw"
///|
pub fn raw(html : String) -> JSXNode {
ffi_raw_esm(html)
}
///|
/// Create HTML from template string (legacy - prefer HonoJsx::html)
#module("hono/html")
extern "js" fn ffi_html_esm(template : FixedArray[String]) -> HtmlEscapedString = "html"
///|
pub fn html(template : String) -> HtmlEscapedString {
ffi_html_esm([template])
}
///|
/// Memo (legacy - prefer HonoJsx::memo)
#module("hono/jsx")
extern "js" fn ffi_memo_esm(component : @core.Any) -> @core.Any = "memo"
///|
pub fn[T] memo(component : FC[T]) -> FC[T] {
ffi_memo_esm(@core.any(component)).cast()
}
///|
/// Create a context (legacy - prefer HonoJsx::create_context)
#module("hono/jsx")
extern "js" fn ffi_create_context_esm(defaultValue : @core.Any) -> @core.Any = "createContext"
///|
pub fn[T] create_context(defaultValue : T?) -> HonoContext[T] {
ffi_create_context_esm(@core.any(defaultValue)).cast()
}
///|
/// Use context value (legacy - prefer HonoJsx::use_context)
#module("hono/jsx")
extern "js" fn ffi_use_context_esm(ctx : @core.Any) -> @core.Any = "useContext"
///|
pub fn[T] use_context(ctx : HonoContext[T]) -> T {
ffi_use_context_esm(ctx.as_any()).cast()
}
///|
/// Context Provider (legacy - prefer HonoJsx::context_provider)
pub fn[T] context_provider(
ctx : HonoContext[T],
children : Array[&HonoNode],
value~ : T,
) -> JSXNode {
let children_any = children.map(child => child.toHonoNode())
let props = @core.new_object()
props["value"] = @core.any(value)
let flat_children : @core.Any = if children_any.length() == 1 {
children_any[0]
} else {
@core.any(children_any)
}
props["children"] = flat_children
ffi_jsxs_esm(ffi_get_provider(ctx.as_any()), props)
}
///|
/// Render JSX to ReadableStream (legacy - prefer HonoJsx::render_to_readable_stream)
#module("hono/jsx/streaming")
extern "js" fn ffi_render_to_readable_stream_esm(node : @core.Any) -> @core.Any = "renderToReadableStream"
///|
pub fn render_to_readable_stream(node : JSXNode) -> @core.Any {
ffi_render_to_readable_stream_esm(node.as_any())
}