msdoc-viewer

High-performance MS-DOC (.doc binary Word) parser and renderer for MoonBit, supporting JS, WASM, and WASM-GC backends with Web Worker support

ms-doc
parser
renderer
word-document
document-processing
wasm
multi-target
moon add XiaoChenxu/msdoc-viewer@0.1.1
Download zip
Version
0.1.1
License
Apache-2.0
Last updated
last month
Downloads
11
README

#MS-DOC Viewer - MoonBit

High-performance MS-DOC (.doc binary Word) parser and renderer, compiled from MoonBit to JS, WASM, and WASM-GC backends.

#Project Status

  • MoonBit国产开源生态大赛 OSC 2026 参赛项目
  • 已发布到 mooncakes.io: https://www.mooncakes.io/
  • CI 验证通过: GitHub Actions 全部成功
  • 测试覆盖: 103 个测试全部通过
  • 项目规模: 13,347 有效 MoonBit 代码行

#Features

  • Multi-target compilation: JS (ESM), WASM (linear memory), WASM-GC backends
  • Web Worker support: Offload parsing to background threads
  • WASM browser support: Both WASM and WASM-GC run directly in browser
  • Modular architecture: Core parser and worker packages
  • Rich content parsing: Text, tables, images (PNG/JPEG/EMF/WMF), headers/footers, textboxes, footnotes/endnotes
  • OLE object extraction: OLE10Native attachments, EPRINT streams, PSD-wrapped EMF
  • Document metadata: Summary Information (title, author, subject)
  • Page layout: Section properties (margins, orientation, columns)

#Multi-Target Build Architecture

One MoonBit source codebase, four build targets:

TargetCommandOutputBrowserNode.jsWASM Runtime
JSmoon build --target js --releasecore.js (~375KB)✅ ESM
JS Workermoon build --target js --release workerworker.js (~380KB)✅ Worker
WASMmoon build --target wasm --releasecore.wasm (~133KB)✅ JS bridge✅ WASMtime
WASM-GCmoon build --target wasm-gc --releasecore.wasm (~97KB)✅ JS string builtins✅ moon run

Build all targets at once:

.\build.bat

#Which Target to Use?

  • Browser (simplest): JS target — direct ESM import, no bridge needed
  • Browser (threaded): JS Worker — parse in background thread, UI stays responsive
  • Browser (performance): WASM — linear memory, near-native computation speed
  • Browser (compact): WASM-GC — smallest binary, native JS string interop via wasm:js-string builtins

#Usage

#JS Target (Browser / Node.js)

import { render_ms_doc } from './_build/js/release/build/core/core.js'; const buffer = await file.arrayBuffer(); const raw = render_ms_doc(new Uint8Array(buffer)); const result = raw._0 || raw; // unwrap raise result document.getElementById('viewer').innerHTML = ` <style>${result.css}</style> ${result.html} `;

#Web Worker Target

// worker_wrapper.js — bridges postMessage to MoonBit exports import { worker_parse, worker_render, worker_parse_to_html } from '../_build/js/release/build/worker/worker.js'; self.onmessage = (e) => { const { type, data, id } = e.data; let result; switch(type) { case 'parse': result = worker_parse(data); break; case 'render': result = worker_render(data); break; case 'parseToHtml': result = worker_parse_to_html(data); break; } self.postMessage({ id, type, result }); }; self.postMessage({ type: 'ready' });

#WASM Target (Browser)

const resp = await fetch('./_build/wasm/release/build/core/core.wasm'); const { instance } = await WebAssembly.instantiate(await resp.arrayBuffer()); const { memory, render_ms_doc } = instance.exports; // Allocate bytes in fresh memory pages (avoids collision with MoonBit heap) function allocBytes(data) { const pageSize = 65536; const headerSize = 8; const prevPages = memory.grow(Math.ceil((headerSize + data.length) / pageSize)); const ptr = prevPages * pageSize + headerSize; const dv = new DataView(memory.buffer); dv.setInt32(ptr - 8, 1, true); // refcount dv.setInt32(ptr - 4, data.length, true); // length new Uint8Array(memory.buffer).set(data, ptr); return ptr; } // Read MoonBit String from linear memory (UTF-16) function readString(ptr) { if (ptr <= 0) return ''; const dv = new DataView(memory.buffer); const len = dv.getInt32(ptr - 4, true) & 0x0FFFFFFF; let str = ''; for (let i = 0; i < len; i++) { str += String.fromCharCode(dv.getUint16(ptr + i * 2, true)); } return str; } // Read RenderResult struct { html: String, css: String } function readRenderResult(structPtr) { const dv = new DataView(memory.buffer); return { html: readString(dv.getInt32(structPtr, true)), css: readString(dv.getInt32(structPtr + 4, true)), }; } // Call: returns (tag:i32, ptr:i32) — tag=1 success, tag=0 error const inputPtr = allocBytes(uint8Array); const [tag, resultPtr] = render_ms_doc(inputPtr); if (tag === 1) { const result = readRenderResult(resultPtr); viewer.innerHTML = `<style>${result.css}</style>${result.html}`; }

#WASM-GC Target (Browser + MoonBit Runtime)

const resp = await fetch('./_build/wasm-gc/release/build/core/core.wasm'); const bytes = await resp.arrayBuffer(); const { instance } = await WebAssembly.instantiate(bytes, {}, { builtins: ['js-string'], // wasm:js-string builtins (Chrome 112+) importedStringConstants: '_' // string constant namespace }); // Binary data passed as hex-encoded strings const hex = uint8ArrayToHex(docBytes); const result = instance.exports.render_ms_doc_wasm(hex); // result is a native JS string: "CSS|||HTML" const sep = result.indexOf('|||SEPARATOR|||'); const css = result.substring(0, sep); const html = result.substring(sep + 15);

Also works with MoonBit's own runtime:

moon run --target wasm-gc test

#MoonBit WASM Linear Memory ABI

This section documents the calling convention for MoonBit's wasm (linear memory) target.

#Object Layout

All MoonBit objects in WASM linear memory follow this pattern:

┌─────────────┬─────────────┬──────────────────────────┐ │ refcount(i32)│ header(i32) │ data │ │ at ptr - 8 │ at ptr - 4 │ starting at ptr │ └─────────────┴─────────────┴──────────────────────────┘

  • refcount (ptr - 8): Reference count (managed by MoonBit runtime)
  • header (ptr - 4): Tag/type info (lower 8 bits) | length (upper 28 bits)
    • For arrays: length & 0x0FFFFFFF gives element count
    • For strings: same mask gives UTF-16 code unit count

#FixedArray[Byte] (Bytes)

ptr - 8: refcount (i32) ptr - 4: byte_length (i32) ptr + 0: raw bytes [byte0, byte1, ...]

#String

ptr - 8: refcount (i32) ptr - 4: char_count (i32, number of UTF-16 code units) ptr + 0: UTF-16LE data [char0_lo, char0_hi, char1_lo, char1_hi, ...]

Strings are UTF-16 encoded (2 bytes per character).

#Function Return Convention

Functions that can raise return (tag: i32, result: i32):

  • tag = 0: Error — result is an error string pointer
  • tag = 1: Success — result is the return value pointer

#Key Differences Between WASM Targets

FeatureWASM (linear memory)WASM-GC
MemoryExported, JS-managedInternal GC
String encodingUTF-16 (2 bytes/char)JS string (externref)
Object accessDirect memory read/writeNative JS string interop
Browser useVia JS memory bridgeVia wasm:js-string builtins
FFI configexport-memory-name: "memory"use-js-builtin-string: true
Browser compatAll modernChrome 112+, Firefox 120+, Safari 16.4+

#API Reference

#Core Functions

FunctionInputReturnsNotes
parse_ms_doc(bytes)FixedArray[Byte]MsDocParseResultParses .doc, returns structured data
render_ms_doc(bytes)FixedArray[Byte]RenderResultParses and renders to HTML+CSS
parse_ms_doc_to_html(bytes)FixedArray[Byte]StringReturns HTML only

#WASM-GC Bridge Functions

FunctionInputReturns
render_ms_doc_wasm(hex)hex-encoded string"CSS\|\|\|HTML"
parse_ms_doc_to_html_wasm(hex)hex-encoded string"CSS\|\|\|HTML"
parse_ms_doc_wasm(hex)hex-encoded stringtext content

#Worker Functions

FunctionInputReturns
worker_parse(bytes)FixedArray[Byte]text string
worker_render(bytes)FixedArray[Byte]"CSS\|\|\|HTML"
worker_parse_to_html(bytes)FixedArray[Byte]"CSS\|\|\|HTML"

#Building from Source

#Prerequisites

  • MoonBit toolchain (tested with moon v0.1.20260703, moonc v0.10.3)

#Build

.\build.bat # All targets moon build --target js --release # JS only moon build --target js --release worker # Worker only moon build --target wasm --release # WASM only moon build --target wasm-gc --release # WASM-GC only moon test # Run tests

#Run Demos

Serve the project root with any HTTP server, then open:

  • demo/index.html — Landing page
  • demo/js.html — JS target demo
  • demo/wasm.html — WASM linear memory demo
  • demo/wasm-gc.html — WASM-GC demo
  • demo/worker.html — Web Worker demo

#Project Structure

msdoc-viewer-moonbit/ ├── core/ # Core parser + renderer (all targets) │ ├── cfb.mbt # CFB/OLE container parser │ ├── fib.mbt # File Information Block parser │ ├── clx.mbt # CLX / Piece Table parser │ ├── parser.mbt # Main MS-DOC parser │ ├── render.mbt # HTML/CSS renderer │ ├── wasm_bridge.mbt # WASM-GC hex-string bridge (conditional) │ ├── moon.pkg # Multi-target exports config │ └── ... # binary, sprm, fkp, fonts, styles, etc. ├── worker/ # Web Worker entry point │ ├── worker_entry.mbt # MoonBit worker logic │ ├── worker_wrapper.js # JS→MoonBit ESM bridge │ └── moon.pkg # is-main + JS exports ├── test/ # Tests │ ├── fixtures/ # Test .doc files (test1~test7.doc) │ └── ... # Unit tests + WASM entry ├── demo/ # Browser demos │ ├── index.html # Landing page │ ├── js.html # JS target demo │ ├── wasm.html # WASM demo │ ├── wasm-gc.html # WASM-GC demo │ └── worker.html # Web Worker demo ├── build.bat # Multi-target build script └── moon.mod.json # MoonBit module config

#Architecture

The MS-DOC format uses a Compound File Binary (CFB) container with multiple streams:

  1. CFB Parser — Extracts streams from OLE container
  2. FIB Parser — Reads File Information Block
  3. CLX Parser — Recovers text via Piece Table
  4. SPRM Decoder — Decodes property modifiers
  5. FKP Reader — Reads format/property key pages
  6. Styles Parser — Parses style definitions
  7. Properties — Converts properties to state objects
  8. Main Parser — Orchestrates the full pipeline
  9. HTML Renderer — Converts parsed data to HTML+CSS

#License

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.