Skip to content
Copied!
published on 2026-05-18

3. TextRenderer

The TextRenderer component calls a WebAssembly function and displays the returned string as-is. Where PPMRenderer renders PPM images onto a <canvas>, TextRenderer is used to display text-format computation results — such as Monte Carlo integration convergence reports — inside a <pre> tag.

Registration follows the same approach as PPMRenderer. Add it to .vitepress/theme/index.js:

.vitepress
└── theme
    ├── components
    │   ├── PPMRenderer.vue
    │   └── TextRenderer.vue
    └── index.js
js
import DefaultTheme from 'vitepress/theme'
import PPMRenderer from './components/PPMRenderer.vue'
import TextRenderer from './components/TextRenderer.vue'

export default {
  extends: DefaultTheme,
  enhanceApp({ app }) {
    app.component('PPMRenderer', PPMRenderer)
    app.component('TextRenderer', TextRenderer)
  }
}

TextRenderer calls the function wasm-function from the specified wasm-module. That function is expected to return a string. The return value is displayed inside a <pre> tag.

The full source of TextRenderer is shown below.

TextRenderer.vue
vue
<template>
  <div class="wasm-text-renderer">
    <div v-if="!loaded && !error" class="loading">
      Running {{ title }}...
    </div>

    <div v-else-if="error" class="error">
      {{ error }}
    </div>

    <div v-else class="result">
      <p v-if="description" class="description">{{ description }}</p>
      <pre class="text-output">{{ output }}</pre>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { withBase } from 'vitepress'

const props = defineProps({
  title: { type: String, required: true },
  wasmFunction: { type: String, required: true },
  wasmModule: { type: String, required: true },
  description: { type: String, default: '' },
})

const loaded = ref(false)
const error = ref(null)
const output = ref('')

onMounted(async () => {
  try {
    const fullPath = withBase(props.wasmModule)
    const response = await fetch(fullPath)
    if (!response.ok) throw new Error(`HTTP ${response.status} fetching ${fullPath}`)
    const jsText = await response.text()
    const blob = new Blob([jsText], { type: 'text/javascript' })
    const blobUrl = URL.createObjectURL(blob)
    let wasm
    try {
      wasm = await import(/* @vite-ignore */ blobUrl)
    } finally {
      URL.revokeObjectURL(blobUrl)
    }

    const wasmBinaryUrl = new URL(fullPath.replace(/\.js$/, '_bg.wasm'), window.location.origin)
    await wasm.default(wasmBinaryUrl)

    const fn = wasm[props.wasmFunction]
    if (typeof fn !== 'function') {
      throw new Error(`WASM function '${props.wasmFunction}' is not available`)
    }

    output.value = String(fn())
    loaded.value = true
  } catch (err) {
    error.value = `Failed to run ${props.title}: ${err.message}`
    console.error('WASM text rendering error:', err)
  }
})
</script>

<style scoped>
.wasm-text-renderer {
  margin: 1.5rem 0;
  padding: 1rem;
  border: 1px solid var(--vp-c-border);
  border-radius: 8px;
  background: var(--vp-c-bg-soft);
}

.loading,
.error {
  padding: 1rem;
}

.error {
  background: var(--vp-c-danger-soft);
  color: var(--vp-c-danger-1);
  border-radius: 6px;
}

.description {
  margin-bottom: 0.75rem;
  color: var(--vp-c-text-2);
}

.text-output {
  margin: 0;
  white-space: pre-wrap;
  word-break: break-word;
  overflow-x: auto;
  background: var(--vp-c-bg);
  border: 1px solid var(--vp-c-divider);
  border-radius: 6px;
  padding: 0.75rem;
}
</style>

Each section is explained below.

Template

First, the Vue template. The div.wasm-text-renderer block switches between three states — loading, error, and result — using v-if / v-else-if / v-else. While processing, Running ... is shown. If an error occurs, its message is displayed instead.

description is optional. It is rendered as a <p> only when truthy, via v-if="description". The output text is displayed inside <pre class="text-output">. The white-space: pre-wrap style preserves all line breaks and spaces.

vue
<template>
  <div class="wasm-text-renderer">
    <div v-if="!loaded && !error" class="loading">
      Running {{ title }}...
    </div>

    <div v-else-if="error" class="error">
      {{ error }}
    </div>

    <div v-else class="result">
      <p v-if="description" class="description">{{ description }}</p>
      <pre class="text-output">{{ output }}</pre>
    </div>
  </div>
</template>

Props

The template accepts four pieces of information: a title, the WASM function name, the WASM module path, and an optional description string.

js
const props = defineProps({
  title: { type: String, required: true },
  wasmFunction: { type: String, required: true },
  wasmModule: { type: String, required: true },
  description: { type: String, default: '' },
})

title is the counterpart to PPMRenderer's imageName and is used in the loading message (Running ...). Unlike PPMRenderer, description is optional (default '').

The calling side (a Markdown file) writes it as follows:

html
<ClientOnly>
  <TextRenderer
    title="Monte Carlo PI Report"
    wasm-module="/wasm/raytracing_demos/raytracing_demos.js"
    wasm-function="report_monte_carlo_pi"
    description="Output of r302-monte-carlo. Shows convergence checkpoints and stratified sampling comparison."
  />
</ClientOnly>

Mounted Callback

This is the code called when the component is mounted.

1. Loading the WASM Module

WASM is loaded using the same fetch + Blob URL pattern as PPMRenderer. See the previous page for details.

js
const fullPath = withBase(props.wasmModule)
const response = await fetch(fullPath)
const jsText = await response.text()
const blob = new Blob([jsText], { type: 'text/javascript' })
const blobUrl = URL.createObjectURL(blob)
let wasm
try {
  wasm = await import(/* @vite-ignore */ blobUrl)
} finally {
  URL.revokeObjectURL(blobUrl)
}
const wasmBinaryUrl = new URL(
  fullPath.replace(/\.js$/, '_bg.wasm'),
  window.location.origin
)
await wasm.default(wasmBinaryUrl)

2. Calling the WASM Function and Displaying Output

The function object is retrieved by name from props and verified to be a function with typeof before being called. The return value is converted to a string with String(), assigned to output.value, and then loaded.value is set to true.

js
const fn = wasm[props.wasmFunction]
if (typeof fn !== 'function') {
  throw new Error(`WASM function '${props.wasmFunction}' is not available`)
}
output.value = String(fn())
loaded.value = true

Unlike PPMRenderer, no canvas resizing or nextTick() is needed. Assigning to output.value is enough for Vue's reactivity to update the <pre> content.