docx2html

Native MoonBit DOCX to HTML/Markdown converter ported from Mammoth

docx
html
markdown
mammoth
moon add bobzhang/docx2html@0.3.0
Download zip
Author
Version
0.3.0
License
Apache-2.0
Last updated
10 days ago
Downloads
436
README

#bobzhang/docx2html

Native MoonBit DOCX reader and converter, ported from Mammoth. The current focus is the common docx -> html, docx -> markdown, and raw-text paths with Mammoth-compatible diagnostics where the JavaScript library exposes them.

#Install

moon add bobzhang/docx2html

#Usage

///|
test "convert document model to html" {
let doc = @docx2html.document([
@docx2html.paragraph([@docx2html.text("Hello.")]),
])
let result = @docx2html.convert_document_to_html(doc)
inspect(result.value, content="<p>Hello.</p>")
}

Use convert or convert_document with output_format=Markdown when the format is chosen dynamically:

///|
test "choose markdown output" {
let doc = @docx2html.document([
@docx2html.paragraph([@docx2html.text("Hello.")]),
])
let result = @docx2html.convert_document(doc, output_format=Markdown)
inspect(result.value, content="Hello\\.\n\n")
}

For DOCX bytes, use convert, convert_to_html, convert_to_markdown, or extract_raw_text. The native API accepts BytesView, so callers can decide how to load files. External linked images stay disabled by default, matching Mammoth; pass external_file_access=true and a read_external_file callback when the source document is allowed to read sibling files. Use read_docx_with_messages when you need the parsed document model together with DOCX reader diagnostics; read_docx is the document-only convenience wrapper.

#Style Maps

Pass explicit style-map lines with style_map. If you already have a Mammoth-style multi-line string, normalize it with read_style_map_string so comments, blank lines, and trimming follow Mammoth's option parser:

///|
test "parse mammoth style map strings" {
let style_map_text =
#|# ignored
#|
#|p[style-name='Heading 1'] => h2
let style_map = @docx2html.read_style_map_string(style_map_text)
let doc = @docx2html.document([
@docx2html.paragraph(
[@docx2html.text("Title")],
properties=@docx2html.paragraph_properties(style_name=Some("Heading 1")),
),
])
let result = @docx2html.convert_document_to_html(doc, style_map~)
inspect(result.value, content="<h2>Title</h2>")
}

Use parse_style_map or parse_style_map_string when you want to validate a style map before conversion while preserving the valid mappings:

///|
test "preflight style map diagnostics" {
let parsed = @docx2html.parse_style_map_string(
"p.SectionTitle => h2\np => span#",
)
inspect(parsed.mappings.length(), content="1")
debug_inspect(
parsed.messages,
content=(
#|[Warning("Did not understand this style mapping, so ignored it: p => span#\nError was at character number 10: Expected end but got unrecognisedCharacter \"#\"")]
),
)
}

#Document Model

The document model exposes enum constructors for pattern matching and lower-snake helpers for building nodes and property records:

///|
test "build document model with helpers" {
let doc = @docx2html.document([
@docx2html.paragraph(
[
@docx2html.hyperlink(
[@docx2html.text("MoonBit")],
href=Some("https://www.moonbitlang.com"),
),
],
properties=@docx2html.paragraph_properties(style_name=Some("Heading 1")),
),
@docx2html.table([
@docx2html.table_row([
@docx2html.table_cell([@docx2html.paragraph([@docx2html.text("Cell")])]),
]),
]),
])
let result = @docx2html.convert_document_to_html(doc, style_map=[
"p[style-name='Heading 1'] => h2",
])
inspect(
result.value,
content="<h2><a href=\"https://www.moonbitlang.com\">MoonBit</a></h2><table><tr><td><p>Cell</p></td></tr></table>",
)
}

Embedded style maps can be inspected or rewritten without a JavaScript-style mutable ZIP object. Use read_embedded_style_map(docx[:]) to read the raw mammoth/style-map part, and embed_style_map(docx[:], "p => h1") to return a new DOCX archive with the style map, relationship entry, and content-type override updated.

#Transforms

Document transforms run before conversion and recurse through child-bearing nodes. Use the typed helpers instead of Mammoth's JavaScript string type names:

///|
test "transform runs before conversion" {
let doc = @docx2html.document([
@docx2html.paragraph([@docx2html.run([@docx2html.text("Hello.")])]),
])
let transform_document = @docx2html.transform_runs(fn(_run) {
@docx2html.run([@docx2html.text("Goodbye.")])
})
let result = @docx2html.convert_document_to_html(doc, transform_document~)
inspect(result.value, content="<p>Goodbye.</p>")
let runs = @docx2html.document_descendants_where(doc, fn(element) {
element is Run(..)
})
inspect(runs.length(), content="1")
}

#Native CLI

From this repository checkout, the native executable mirrors the common Mammoth CLI paths:

moon run --target native cmd/docx2html -- input.docx moon run --target native cmd/docx2html -- --output-format=markdown input.docx moon run --target native cmd/docx2html -- --style-map style-map input.docx output.html moon run --target native cmd/docx2html -- --output-dir out input.docx

The executable examples in tests/cram/cli.md are verified by:

moon cram test tests/cram

#Image Conversion

Images are emitted as data URIs by default. Pass convert_image to override that behavior. data_uri_image is the default converter, and inline_image is the MoonBit name for Mammoth's images.inline/images.imgElement helper:

///|
test "custom image conversion" {
let image = @docx2html.Image::{
content_type: "image/png",
alt_text: Some("chart"),
data: b"abc",
}
let doc = @docx2html.document([@docx2html.paragraph([Image(image)])])
let convert_image = @docx2html.inline_image(fn(_image) {
{ "src": "/assets/chart.png" }
})
let result = @docx2html.convert_document_to_html(doc, convert_image~)
inspect(
result.value,
content="<p><img alt=\"chart\" src=\"/assets/chart.png\" /></p>",
)
}

Image converters return diagnostics explicitly instead of throwing:

///|
test "custom image conversion diagnostics" {
let image = @docx2html.image("image/png", b"abc", alt_text=Some("chart"))
let convert_image = fn(_image : @docx2html.Image) {
@docx2html.image_conversion([], messages=[@docx2html.error("image omitted")])
}
let result = @docx2html.convert_document_to_html(image, convert_image~)
inspect(result.value, content="")
debug_inspect(result.messages, content="[Error(\"image omitted\")]")
}

#Results

Conversion results carry rendered text plus diagnostics. Use combine_results when stitching several conversion fragments together:

///|
test "combine conversion results" {
let combined = @docx2html.combine_results([
@docx2html.success("One"),
{ value: "Two", messages: [Warning("same")] },
{ value: "Three", messages: [Warning("same")] },
])
inspect(combined.value, content="OneTwoThree")
debug_inspect(combined.messages, content="[Warning(\"same\")]")
}

#Status

This is an active native-first port. The verified scope covers the core docx -> html, docx -> markdown, and raw-text paths, plus embedded style maps, external-image loader callbacks, notes, comments, tables, hyperlinks, complex fields, XML/ZIP helpers, and the native CLI. See MammothParity.md for the current parity ledger, StressTesting.md for large-file comparison notes, and the intentional API differences from Mammoth's JavaScript surface.

#
BreakType

Kind of break represented in the document model.

#
Comment

Word comment metadata and body content.

#
ConversionResult

Converted value together with accumulated diagnostics.

#
DocumentElement

Node in the Mammoth-style document tree.

#
DocumentMatcher

Matcher for document elements in style-map rules.

#
DocxPackageResult

Package-level read result: body document plus sections and header/footer stories. Returned by read_docx_package.

#
DocxReadResult

Document tree plus diagnostics produced while reading a DOCX package.

#
DocxXmlResourceLimit

Machine-readable XML work or allocation guard raised by DOCX processing.

#
HtmlNode

Node in the intermediate HTML tree.

#
HtmlPathElement

One element in a parsed style-map HTML path.

#
Image

Image payload and metadata read from a DOCX package.

#
ImageConversion

Rendered image nodes plus any diagnostics from image conversion.

#
Indent

Paragraph indentation values from WordprocessingML.

#
Message

Diagnostic emitted while reading or converting a document.

#
Note

Footnote or endnote content in the document model.

#
Numbering

List numbering metadata attached to a paragraph.

#
OutputFormat

Selects whether conversion renders HTML or Markdown.

#
ParagraphProperties

Formatting metadata attached to a paragraph.

#
RunProperties

Formatting metadata attached to a run.

#
StyleMapParseResult

Parsed style mappings together with style-map diagnostics.

#
StyleMapping

Parsed mapping from a document matcher to an HTML path.

#
StyleNameMatcher

Matcher for style names in style-map rules.

#
TableProperties

Formatting metadata attached to a table.

#
VerticalAlignment

Vertical positioning for text within a run.

#
XmlElement

XML element with a name, attributes, and child nodes.

#
XmlNode

Node in the lightweight XML tree.

#
ZipArchive

In-memory ZIP archive used to read DOCX parts.

#
DocxError

pub(all) suberror DocxError {
InvalidZip(message~ : String)
InvalidXml(message~ : String)
MissingPart(message~ : String)
Unsupported(message~ : String)
ResourceLimit(limit~ :
DocxXmlResourceLimit
, message~ : String)
WriteResourceLimit(kind~ : String, limit~ : Int64, actual~ : Int64, message~ : String)
} derive(Eq,
Debug
)

Errors raised for invalid archives, invalid XML, missing parts, or unsupported input.

#
DocxError::equal

#deprecated("implicit trait-method promotion is being removed; call via the trait")
fn DocxError::equal(DocxError, DocxError) -> Bool

#
DocxError::not_equal

#deprecated("implicit trait-method promotion is being removed; call via the trait")
fn DocxError::not_equal(x : DocxError, y : DocxError) -> Bool

#
DocxError::to_repr

#deprecated("implicit trait-method promotion is being removed; call via the trait")
fn DocxError::to_repr(DocxError) ->
Repr

#
bookmark_start

Builds a bookmark-start element.

#
checkbox

Builds a checkbox element.

#
column_break

Builds a column-break element.

#
combine_results

Combines conversion results, preserving values and diagnostics.

#
comment

fn comment(comment_id : String, body : Array[
DocumentElement
], author_name? : String, author_initials? : String) ->
Comment

Builds a document comment.

#
comment_reference

fn comment_reference(comment_id : String) ->
DocumentElement

Builds a comment-reference element.

#
comment_reply

fn comment_reply(author~ : String, initials? : String, date? : String, reply_to~ : Int, done? : Bool, body : Array[
DocumentElement
]) ->
CommentSpec
raise DocxError

Builds an anchorless REPLY to an earlier comment spec (by its index in the array passed to write_docx_with_comments); the thread linkage lands in word/commentsExtended.xml. See @docx.comment_reply.

#
comment_spec

fn comment_spec(author~ : String, initials? : String, date? : String, from~ : Int, to~ : Int, done? : Bool, body : Array[
DocumentElement
]) ->
CommentSpec
raise DocxError

Validates and builds one comment for write_docx_with_comments: author/initials/date metadata, the inclusive 0-based range of body block indexes it anchors (from..to, both endpoints top-level paragraphs), and its paragraph-only plain-content body. See @docx.comment_spec for the fail-closed rules (non-empty author, lexical xsd:dateTime date, ordered range, non-empty body).

#
convert

fn convert(docx : BytesView, output_format? :
OutputFormat
, style_map? : Array[String], include_default_style_map? : Bool, include_embedded_style_map? : Bool, ignore_empty_paragraphs? : Bool, id_prefix? : String, pretty_print? : Bool, convert_image? : (
Image
) ->
ImageConversion
, transform_document? : (
DocumentElement
) ->
DocumentElement
, external_file_access? : Bool, read_external_file? : (String) -> Bytes?) ->
ConversionResult
raise DocxError

Converts DOCX bytes using the requested output format.

#
convert_document

Converts a document tree using the requested output format.

#
convert_document_to_html

fn convert_document_to_html(document :
DocumentElement
, style_map? : Array[String], include_default_style_map? : Bool, ignore_empty_paragraphs? : Bool, id_prefix? : String, pretty_print? : Bool, convert_image? : (
Image
) ->
ImageConversion
, transform_document? : (
DocumentElement
) ->
DocumentElement
) ->
ConversionResult

Converts a document tree to HTML.

#
convert_document_to_markdown

Converts a document tree to Markdown.

#
convert_to_html

fn convert_to_html(docx : BytesView, style_map? : Array[String], include_default_style_map? : Bool, include_embedded_style_map? : Bool, ignore_empty_paragraphs? : Bool, id_prefix? : String, pretty_print? : Bool, convert_image? : (
Image
) ->
ImageConversion
, transform_document? : (
DocumentElement
) ->
DocumentElement
, external_file_access? : Bool, read_external_file? : (String) -> Bytes?) ->
ConversionResult
raise DocxError

Converts DOCX bytes to HTML.

#
convert_to_markdown

fn convert_to_markdown(docx : BytesView, style_map? : Array[String], include_default_style_map? : Bool, include_embedded_style_map? : Bool, ignore_empty_paragraphs? : Bool, id_prefix? : String, convert_image? : (
Image
) ->
ImageConversion
, transform_document? : (
DocumentElement
) ->
DocumentElement
, external_file_access? : Bool, read_external_file? : (String) -> Bytes?) ->
ConversionResult
raise DocxError

Converts DOCX bytes to Markdown.

#
data_uri_image

Converts one image to an inline data URI image result.

#
data_uri_image_converter

Converts images to inline data URI image nodes.

#
default_style_map_lines

fn default_style_map_lines() -> Array[String]

Returns Mammoth-compatible default style-map lines.

#
document_descendants

Returns all descendants of a document element.

#
document_descendants_where

Returns descendants that satisfy a predicate.

#
embed_style_map

fn embed_style_map(docx : BytesView, style_map : String) -> Bytes raise DocxError

Embeds or replaces the DOCX style map part.

#
error

fn error(message : String) ->
Message

Creates an error diagnostic.

#
escape_html

fn escape_html(value : String) -> String

Escapes text for HTML content.

#
escape_html_attribute

fn escape_html_attribute(value : String) -> String

Escapes text for an HTML attribute value.

#
escape_markdown_text

fn escape_markdown_text(value : String) -> String

Escapes text for Markdown output.

#
extract_raw_text

fn extract_raw_text(docx : BytesView, external_file_access? : Bool, read_external_file? : (String) -> Bytes?) ->
ConversionResult
raise DocxError

Extracts raw text from DOCX bytes.

#
extract_raw_text_from_document

Extracts raw text from a document tree.

#
fresh_html_element

fn fresh_html_element(tag : String, attributes? : Map[String, String], children? : Array[
HtmlNode
], separator? : String) ->
HtmlNode

Builds an HTML element with a fresh generated id.

#
html_element

fn html_element(tag : String, attributes? : Map[String, String], children? : Array[
HtmlNode
], fresh? : Bool, separator? : String) ->
HtmlNode

Builds an HTML element node.

#
html_text

fn html_text(value : String) ->
HtmlNode

Builds an HTML text node.
fn hyperlink(children : Array[
DocumentElement
], href? : String?, anchor? : String?, target_frame? : String?) ->
DocumentElement

Builds a hyperlink element.

#
identity_document_transform

Returns a document element unchanged.

#
image

fn image(content_type : String, data : Bytes, alt_text? : String?) ->
DocumentElement

Builds an image element.

#
image_conversion

Builds an image conversion result from rendered nodes and diagnostics.

#
img_element

Creates an image converter that emits an img element.

#
indent

fn indent(start? : String?, end? : String?, first_line? : String?, hanging? : String?) ->
Indent

Builds paragraph indentation metadata.

#
inline_image

Alias for img_element matching Mammoth image API style.

#
join_zip_path

fn join_zip_path(parts : Array[String]) -> String

Joins ZIP path components with normalized separators.

#
line_break

Builds a line-break element.

#
new_blank_docx

fn new_blank_docx() -> Bytes

Builds a minimal, schema-valid blank docx (one empty paragraph, Normal style, Letter page). See @docx.new_blank_docx.

#
note

Builds a footnote or endnote.

#
note_reference

fn note_reference(note_type : String, note_id : String) ->
DocumentElement

Builds a note-reference element.

#
note_spec

Validates and builds one footnote/endnote body for write_docx_with_annotations (which array it goes in decides the kind). Plain-content, paragraph-only, no nested notes. See @docx.note_spec.

#
numbering

fn numbering(is_ordered : Bool, level : Int) ->
Numbering

Builds paragraph numbering metadata.

#
open_zip

fn open_zip(data : BytesView) ->
ZipArchive
raise DocxError

Opens ZIP bytes into an in-memory archive.

#
page_break

Builds a page-break element.

#
paragraph_properties

fn paragraph_properties(style_id? : String?, style_name? : String?, numbering? :
Numbering
?, alignment? : String?, indent? :
Indent
) ->
ParagraphProperties

Builds paragraph formatting metadata.

#
parse_style_map

Parses style-map lines and preserves diagnostics.

#
parse_style_map_string

fn parse_style_map_string(style_map : String) ->
StyleMapParseResult

Parses a style-map string and preserves diagnostics.

#
read_docx

fn read_docx(docx : BytesView, external_file_access? : Bool, read_external_file? : (String) -> Bytes?) ->
DocumentElement
raise DocxError

Reads DOCX bytes into a document tree.

#
read_docx_annotated

fn read_docx_annotated(docx : BytesView, external_file_access? : Bool, read_external_file? : (String) -> Bytes?) ->
DocxAnnotatedResult
raise DocxError

Reads DOCX bytes into the package representation PLUS the annotation index (comments with threading and anchors, note references). See @docx.read_docx_annotated; the package half is identical to read_docx_package.

#
read_docx_package

fn read_docx_package(docx : BytesView, external_file_access? : Bool, read_external_file? : (String) -> Bytes?) ->
DocxPackageResult
raise DocxError

Reads DOCX bytes into the package-level representation: the body document (identical to read_docx_with_messages) plus sections and header/footer stories. See @docx.read_docx_package.

#
read_docx_with_messages

fn read_docx_with_messages(docx : BytesView, external_file_access? : Bool, read_external_file? : (String) -> Bytes?) ->
DocxReadResult
raise DocxError

Reads DOCX bytes into a document tree and diagnostics.

#
read_embedded_style_map

fn read_embedded_style_map(docx : BytesView) -> String? raise DocxError

Reads the embedded style map from DOCX bytes, if present.

#
read_style_map

Parses style-map lines and returns valid mappings.

#
read_style_map_string

fn read_style_map_string(style_map : String) -> Array[String]

Splits a style-map string into meaningful lines.

#
read_style_mapping

fn read_style_mapping(line : String) ->
StyleMapping
?

Parses one style-map line into a mapping, if valid.

#
read_xml_string

fn read_xml_string(value : String, namespace_map? : Map[String, String]) ->
XmlElement
raise DocxError

Parses XML text into an element.

#
run_properties

fn run_properties(style_id? : String?, style_name? : String?, is_bold? : Bool, is_underline? : Bool, is_italic? : Bool, is_strikethrough? : Bool, is_all_caps? : Bool, is_small_caps? : Bool, vertical_alignment? :
VerticalAlignment
, font? : String?, font_size? : Int?, highlight? : String?) ->
RunProperties

Builds run formatting metadata.

#
simplify_html

Simplifies adjacent and redundant HTML nodes.

#
split_zip_path

fn split_zip_path(path : String) -> (String, String)

Splits a ZIP path into directory and filename.

#
success

Creates a successful conversion result with no diagnostics.

#
table_cell

Builds a table-cell element.

#
table_properties

fn table_properties(style_id? : String?, style_name? : String?) ->
TableProperties

Builds table formatting metadata.

#
table_row

Builds a table-row element.

#
text

Builds a text element.

#
warning

fn warning(message : String) ->
Message

Creates a warning diagnostic.

#
write_docx

Serializes semantic body content (paragraphs and runs, F1 surface) into a schema-valid docx. Fail-closed: unsupported elements raise Unsupported rather than dropping content. See @docx.write_docx.

#
write_docx_with_annotations

The full annotation writer: write_docx plus comments, footnotes, and endnotes. Body runs reference notes with note_reference(kind, index) (0-based into the matching array; exactly one reference per note). See @docx.write_docx_with_annotations.

#
write_docx_with_comments

write_docx plus comments: anchors are emitted into the anchored paragraphs in the canonical shape and the definitions land in word/comments.xml as a main-part relationship, with dense ids (a spec's array index is its w:id). See @docx.write_docx_with_comments.

#
write_html

fn write_html(nodes : Array[
HtmlNode
], pretty_print? : Bool) -> String

Serializes HTML nodes to a string.

#
write_markdown

fn write_markdown(nodes : Array[
HtmlNode
]) -> String

Serializes HTML nodes to Markdown.

#
write_xml_string

fn write_xml_string(element :
XmlElement
, namespaces? : Map[String, String]) -> String

Serializes an XML element to text.

#
xml_element

fn xml_element(name : String, attributes? : Map[String, String], children? : Array[
XmlNode
]) ->
XmlElement

Builds an XML element.

#
xml_text

fn xml_text(value : String) ->
XmlNode

Builds an XML text node.

#
zip_archive

fn zip_archive(entries : Map[String, BytesView]) ->
ZipArchive
raise DocxError

Builds a ZIP view from an entry map.