Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .agents/docs/2026-10-27-add-md4c-mwrap-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Add mcpplibs.mwrap.md4c (md4c-m 0.5.3)

## Form

- Source: third-party upstream [mity/md4c](https://github.com/mity/md4c), latest tag `release-0.5.3`
(`git ls-remote --tags`), MIT. The tarball (`353c346f…059b9e`) is the one compat.md4c 0.5.3 already pins.
- Form A external mwrap module repository: [mcpplibs/md4c-m](https://github.com/mcpplibs/md4c-m), package
`mcpplibs.mwrap.md4c`, version `0.5.3` (the upstream version, first release of the identity, no revision
suffix), tag series `mwrap-${version}`.
- Implementation ownership: the parser `md4c.c` is compat.md4c's (its configuration and ABI are sufficient).
Upstream ships a second library, md4c-html (`md4c-html.c`, `entity.c`), which compat.md4c does not build;
md4c-m vendors those four files byte for byte under `upstream/md4c/src` (checksums in
`upstream/documents.json`). No implementation is linked twice.

## Modules

- `mcpplibs.mwrap.md4c` — every typedef, enum, enumerator, struct and `md_parse` of `<md4c.h>` by
`export using`, re-exporting the two below.
- `mcpplibs.mwrap.md4c.html` — `md_html`.
- `mcpplibs.mwrap.md4c.macros` — the 23 object-like `MD_FLAG_*` / `MD_DIALECT_*` / `MD_HTML_FLAG_*` macros as
`inline constexpr unsigned` in `mcpplibs::mwrap::md4c::macros`. md4c has no function-like or control-flow
macros, so there is no side header; a TU that also includes `<md4c.h>` must set the macros aside to name the
mapped constants (tested in `tests/header_mix_test.cpp`).
- `tools/check_exports.py` parses both headers and fails when a public name or flag macro is not exported
(negative-checked by removing one export).

## Verification

md4c-m CI (Linux GCC 16.1 and LLVM 23.1.3, macOS LLVM, Windows LLVM, all required): build, gtest suites
(parser callbacks and details, renderer, header mixing, macros), the export audit, and upstream's own
`test/run-testsuite.py` over all 11 suites through the `md2html` example (CommonMark 0.31.2 spec 652/652,
extensions, regressions and coverage: 808 examples). Windows needed binary standard streams in md2html (text
mode rewrote line endings). The templates job scaffolds the default `render` template as an installed
package (`tools/test_template.py`) and runs it.

Here, with the CI-pinned mcpp 2026.10.26.1 and `MCPP_INDEX_MIRROR=GLOBAL`:

- `bash tests/run_members.sh md4c-render` — the real release archive through the candidate index; the
consumer parses with callbacks (heading level, table cells, task item) and renders GFM HTML:
`test result ok`.
- `python3 tests/mwrap/check_installed_template.py --package mcpplibs.mwrap.md4c@0.5.3` — `mcpp new` from the
indexed archive, build and run of the unmodified template: "Verified real indexed archive installation and
unmodified template consumer".
- `mcpp xpkg parse pkgs/mwrap/md4c.lua` (form A, three platforms) and the lint job's Lua checks.

## Mirror and updates

No `mcpp-res` access in this change, so the descriptor uses the GitHub release URL as a plain string (CN
users fall back to it); a maintainer can add the GitCode mirror with identical bytes. `extensions.autoupdate`
follows the repository's own `mwrap-${version}` tags.

## Motivation

openmui's Markdown component (openmui-markdown → openmui-ui-markdown) parses with md4c through this module:
its document model is checked byte for byte against `md_html` on every upstream example.
5 changes: 5 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1603,6 +1603,11 @@ jobs:
shell: bash
run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.imgui@1.92.8

- name: Installed md4c wrapper archive and actual default render template
if: contains(format(' {0} ', env.MEMBERS), ' md4c-render ') && matrix.toolchain == 'default'
shell: bash
run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.md4c@0.5.3

- name: Installed Godot wrapper archive and actual standalone math template
if: contains(format(' {0} ', env.MEMBERS), ' godot-math ') && matrix.toolchain == 'default'
shell: bash
Expand Down
1 change: 1 addition & 0 deletions mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ members = [
"tests/mwrap/llamacpp-cpu",
"tests/mwrap/ffmpeg-cpu",
"tests/mwrap/imgui-headless",
"tests/mwrap/md4c-render",
"tests/mwrap/grpc-cpu",
"tests/mwrap/grpc-tools",
"tests/mwrap/godot-math",
Expand Down
38 changes: 38 additions & 0 deletions pkgs/mwrap/md4c.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
-- Immutable upstream-aligned mwrap source release (md4c-m mwrap-0.5.3).
-- The CN mirror is added by a maintainer with mcpp-res access; until then CN
-- users fall back to the GitHub release (docs/cn-mirror.md).
package = {
spec = "1",
namespace = "mcpplibs.mwrap",
name = "md4c",
description = "md4c-m: MD4C (CommonMark / GFM Markdown parser and md4c-html renderer) as C++23 modules",
licenses = { "Apache-2.0" },
repo = "https://github.com/mcpplibs/md4c-m",
type = "package",
extensions = {
autoupdate = {
tag = "mwrap-${version}",
url = "https://github.com/mcpplibs/md4c-m/releases/download/${tag}/md4c-m-${version}.tar.gz",
},
},
xpm = {
linux = {
["0.5.3"] = {
url = "https://github.com/mcpplibs/md4c-m/releases/download/mwrap-0.5.3/md4c-m-0.5.3.tar.gz",
sha256 = "10f7cdffab919bc97e2cf713c0a784e698568eca8a2331ab386515e5bf7743ac",
},
},
macosx = {
["0.5.3"] = {
url = "https://github.com/mcpplibs/md4c-m/releases/download/mwrap-0.5.3/md4c-m-0.5.3.tar.gz",
sha256 = "10f7cdffab919bc97e2cf713c0a784e698568eca8a2331ab386515e5bf7743ac",
},
},
windows = {
["0.5.3"] = {
url = "https://github.com/mcpplibs/md4c-m/releases/download/mwrap-0.5.3/md4c-m-0.5.3.tar.gz",
sha256 = "10f7cdffab919bc97e2cf713c0a784e698568eca8a2331ab386515e5bf7743ac",
},
},
},
}
11 changes: 11 additions & 0 deletions tests/mwrap/md4c-render/mcpp.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "md4c-render"
version = "0.5.3"
description = "Consume the immutable md4c wrapper: parse with callbacks and render GFM to HTML"

[indices]
# Use the complete candidate built-in index prepared by tests/run_members.sh.
# An explicit empty table overrides the legacy workspace redirect.

[dependencies]
"mcpplibs.mwrap.md4c" = "0.5.3"
42 changes: 42 additions & 0 deletions tests/mwrap/md4c-render/tests/behavior.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import std;
import mcpplibs.mwrap.md4c;

namespace macros = mcpplibs::mwrap::md4c::macros;

int main() {
const std::string_view md = "# Title\n\n- [x] done\n\n| a | b |\n|:-|-:|\n| 1 | 2 |\n\n~~old~~ www.example.com &copy;\n";

// md_html (md4c-html, vendored by the wrapper) with GitHub's dialect.
std::string html;
if (md_html(md.data(), static_cast<MD_SIZE>(md.size()),
[](const MD_CHAR* s, MD_SIZE n, void* out) { static_cast<std::string*>(out)->append(s, n); }, &html,
macros::MD_DIALECT_GITHUB, 0) != 0)
return 1;
for (std::string_view want : {"<h1>Title</h1>", "checked", "<th align=\"left\">a</th>", "<td align=\"right\">2</td>", "<del>old</del>",
"<a href=\"http://www.example.com\">", "\xC2\xA9"})
if (!html.contains(want)) return 2;

// md_parse (compat.md4c's parser) with callbacks and details.
struct Seen {
unsigned heading = 0, cells = 0;
int task = 0;
} seen;
MD_PARSER p{};
p.flags = macros::MD_DIALECT_GITHUB;
p.enter_block = [](MD_BLOCKTYPE t, void* d, void* u) {
auto& s = *static_cast<Seen*>(u);
if (t == MD_BLOCK_H) s.heading = static_cast<MD_BLOCK_H_DETAIL*>(d)->level;
if (t == MD_BLOCK_TD || t == MD_BLOCK_TH) ++s.cells;
if (t == MD_BLOCK_LI) s.task = static_cast<MD_BLOCK_LI_DETAIL*>(d)->is_task;
return 0;
};
p.leave_block = [](MD_BLOCKTYPE, void*, void*) { return 0; };
p.enter_span = [](MD_SPANTYPE, void*, void*) { return 0; };
p.leave_span = [](MD_SPANTYPE, void*, void*) { return 0; };
p.text = [](MD_TEXTTYPE, const MD_CHAR*, MD_SIZE, void*) { return 0; };
if (md_parse(md.data(), static_cast<MD_SIZE>(md.size()), &p, &seen) != 0) return 3;
if (seen.heading != 1 || seen.cells != 4 || seen.task != 1) return 4;

std::println("md4c 0.5.3 through import mcpplibs.mwrap.md4c: {} bytes of HTML, {} table cells", html.size(), seen.cells);
return 0;
}
Loading