Skip to content

Commit 9fc59e0

Browse files
authored
mcpplibs.mwrap.md4c 0.5.3: md4c-m (MD4C parser and md4c-html renderer as C++23 modules) (#578)
Descriptor pkgs/mwrap/md4c.lua (form A, the immutable mwrap-0.5.3 release asset), consumer tests/mwrap/md4c-render (parse with callbacks, render GFM HTML), the installed-template check in validate.yml, and the plan.
1 parent 4e56dee commit 9fc59e0

6 files changed

Lines changed: 152 additions & 0 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Add mcpplibs.mwrap.md4c (md4c-m 0.5.3)
2+
3+
## Form
4+
5+
- Source: third-party upstream [mity/md4c](https://github.com/mity/md4c), latest tag `release-0.5.3`
6+
(`git ls-remote --tags`), MIT. The tarball (`353c346f…059b9e`) is the one compat.md4c 0.5.3 already pins.
7+
- Form A external mwrap module repository: [mcpplibs/md4c-m](https://github.com/mcpplibs/md4c-m), package
8+
`mcpplibs.mwrap.md4c`, version `0.5.3` (the upstream version, first release of the identity, no revision
9+
suffix), tag series `mwrap-${version}`.
10+
- Implementation ownership: the parser `md4c.c` is compat.md4c's (its configuration and ABI are sufficient).
11+
Upstream ships a second library, md4c-html (`md4c-html.c`, `entity.c`), which compat.md4c does not build;
12+
md4c-m vendors those four files byte for byte under `upstream/md4c/src` (checksums in
13+
`upstream/documents.json`). No implementation is linked twice.
14+
15+
## Modules
16+
17+
- `mcpplibs.mwrap.md4c` — every typedef, enum, enumerator, struct and `md_parse` of `<md4c.h>` by
18+
`export using`, re-exporting the two below.
19+
- `mcpplibs.mwrap.md4c.html` — `md_html`.
20+
- `mcpplibs.mwrap.md4c.macros` — the 23 object-like `MD_FLAG_*` / `MD_DIALECT_*` / `MD_HTML_FLAG_*` macros as
21+
`inline constexpr unsigned` in `mcpplibs::mwrap::md4c::macros`. md4c has no function-like or control-flow
22+
macros, so there is no side header; a TU that also includes `<md4c.h>` must set the macros aside to name the
23+
mapped constants (tested in `tests/header_mix_test.cpp`).
24+
- `tools/check_exports.py` parses both headers and fails when a public name or flag macro is not exported
25+
(negative-checked by removing one export).
26+
27+
## Verification
28+
29+
md4c-m CI (Linux GCC 16.1 and LLVM 23.1.3, macOS LLVM, Windows LLVM, all required): build, gtest suites
30+
(parser callbacks and details, renderer, header mixing, macros), the export audit, and upstream's own
31+
`test/run-testsuite.py` over all 11 suites through the `md2html` example (CommonMark 0.31.2 spec 652/652,
32+
extensions, regressions and coverage: 808 examples). Windows needed binary standard streams in md2html (text
33+
mode rewrote line endings). The templates job scaffolds the default `render` template as an installed
34+
package (`tools/test_template.py`) and runs it.
35+
36+
Here, with the CI-pinned mcpp 2026.10.26.1 and `MCPP_INDEX_MIRROR=GLOBAL`:
37+
38+
- `bash tests/run_members.sh md4c-render` — the real release archive through the candidate index; the
39+
consumer parses with callbacks (heading level, table cells, task item) and renders GFM HTML:
40+
`test result ok`.
41+
- `python3 tests/mwrap/check_installed_template.py --package mcpplibs.mwrap.md4c@0.5.3` — `mcpp new` from the
42+
indexed archive, build and run of the unmodified template: "Verified real indexed archive installation and
43+
unmodified template consumer".
44+
- `mcpp xpkg parse pkgs/mwrap/md4c.lua` (form A, three platforms) and the lint job's Lua checks.
45+
46+
## Mirror and updates
47+
48+
No `mcpp-res` access in this change, so the descriptor uses the GitHub release URL as a plain string (CN
49+
users fall back to it); a maintainer can add the GitCode mirror with identical bytes. `extensions.autoupdate`
50+
follows the repository's own `mwrap-${version}` tags.
51+
52+
## Motivation
53+
54+
openmui's Markdown component (openmui-markdown → openmui-ui-markdown) parses with md4c through this module:
55+
its document model is checked byte for byte against `md_html` on every upstream example.

‎.github/workflows/validate.yml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1603,6 +1603,11 @@ jobs:
16031603
shell: bash
16041604
run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.imgui@1.92.8
16051605

1606+
- name: Installed md4c wrapper archive and actual default render template
1607+
if: contains(format(' {0} ', env.MEMBERS), ' md4c-render ') && matrix.toolchain == 'default'
1608+
shell: bash
1609+
run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.md4c@0.5.3
1610+
16061611
- name: Installed Godot wrapper archive and actual standalone math template
16071612
if: contains(format(' {0} ', env.MEMBERS), ' godot-math ') && matrix.toolchain == 'default'
16081613
shell: bash

‎mcpp.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ members = [
1616
"tests/mwrap/llamacpp-cpu",
1717
"tests/mwrap/ffmpeg-cpu",
1818
"tests/mwrap/imgui-headless",
19+
"tests/mwrap/md4c-render",
1920
"tests/mwrap/grpc-cpu",
2021
"tests/mwrap/grpc-tools",
2122
"tests/mwrap/godot-math",

‎pkgs/mwrap/md4c.lua‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
-- Immutable upstream-aligned mwrap source release (md4c-m mwrap-0.5.3).
2+
-- The CN mirror is added by a maintainer with mcpp-res access; until then CN
3+
-- users fall back to the GitHub release (docs/cn-mirror.md).
4+
package = {
5+
spec = "1",
6+
namespace = "mcpplibs.mwrap",
7+
name = "md4c",
8+
description = "md4c-m: MD4C (CommonMark / GFM Markdown parser and md4c-html renderer) as C++23 modules",
9+
licenses = { "Apache-2.0" },
10+
repo = "https://github.com/mcpplibs/md4c-m",
11+
type = "package",
12+
extensions = {
13+
autoupdate = {
14+
tag = "mwrap-${version}",
15+
url = "https://github.com/mcpplibs/md4c-m/releases/download/${tag}/md4c-m-${version}.tar.gz",
16+
},
17+
},
18+
xpm = {
19+
linux = {
20+
["0.5.3"] = {
21+
url = "https://github.com/mcpplibs/md4c-m/releases/download/mwrap-0.5.3/md4c-m-0.5.3.tar.gz",
22+
sha256 = "10f7cdffab919bc97e2cf713c0a784e698568eca8a2331ab386515e5bf7743ac",
23+
},
24+
},
25+
macosx = {
26+
["0.5.3"] = {
27+
url = "https://github.com/mcpplibs/md4c-m/releases/download/mwrap-0.5.3/md4c-m-0.5.3.tar.gz",
28+
sha256 = "10f7cdffab919bc97e2cf713c0a784e698568eca8a2331ab386515e5bf7743ac",
29+
},
30+
},
31+
windows = {
32+
["0.5.3"] = {
33+
url = "https://github.com/mcpplibs/md4c-m/releases/download/mwrap-0.5.3/md4c-m-0.5.3.tar.gz",
34+
sha256 = "10f7cdffab919bc97e2cf713c0a784e698568eca8a2331ab386515e5bf7743ac",
35+
},
36+
},
37+
},
38+
}

‎tests/mwrap/md4c-render/mcpp.toml‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "md4c-render"
3+
version = "0.5.3"
4+
description = "Consume the immutable md4c wrapper: parse with callbacks and render GFM to HTML"
5+
6+
[indices]
7+
# Use the complete candidate built-in index prepared by tests/run_members.sh.
8+
# An explicit empty table overrides the legacy workspace redirect.
9+
10+
[dependencies]
11+
"mcpplibs.mwrap.md4c" = "0.5.3"
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import std;
2+
import mcpplibs.mwrap.md4c;
3+
4+
namespace macros = mcpplibs::mwrap::md4c::macros;
5+
6+
int main() {
7+
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";
8+
9+
// md_html (md4c-html, vendored by the wrapper) with GitHub's dialect.
10+
std::string html;
11+
if (md_html(md.data(), static_cast<MD_SIZE>(md.size()),
12+
[](const MD_CHAR* s, MD_SIZE n, void* out) { static_cast<std::string*>(out)->append(s, n); }, &html,
13+
macros::MD_DIALECT_GITHUB, 0) != 0)
14+
return 1;
15+
for (std::string_view want : {"<h1>Title</h1>", "checked", "<th align=\"left\">a</th>", "<td align=\"right\">2</td>", "<del>old</del>",
16+
"<a href=\"http://www.example.com\">", "\xC2\xA9"})
17+
if (!html.contains(want)) return 2;
18+
19+
// md_parse (compat.md4c's parser) with callbacks and details.
20+
struct Seen {
21+
unsigned heading = 0, cells = 0;
22+
int task = 0;
23+
} seen;
24+
MD_PARSER p{};
25+
p.flags = macros::MD_DIALECT_GITHUB;
26+
p.enter_block = [](MD_BLOCKTYPE t, void* d, void* u) {
27+
auto& s = *static_cast<Seen*>(u);
28+
if (t == MD_BLOCK_H) s.heading = static_cast<MD_BLOCK_H_DETAIL*>(d)->level;
29+
if (t == MD_BLOCK_TD || t == MD_BLOCK_TH) ++s.cells;
30+
if (t == MD_BLOCK_LI) s.task = static_cast<MD_BLOCK_LI_DETAIL*>(d)->is_task;
31+
return 0;
32+
};
33+
p.leave_block = [](MD_BLOCKTYPE, void*, void*) { return 0; };
34+
p.enter_span = [](MD_SPANTYPE, void*, void*) { return 0; };
35+
p.leave_span = [](MD_SPANTYPE, void*, void*) { return 0; };
36+
p.text = [](MD_TEXTTYPE, const MD_CHAR*, MD_SIZE, void*) { return 0; };
37+
if (md_parse(md.data(), static_cast<MD_SIZE>(md.size()), &p, &seen) != 0) return 3;
38+
if (seen.heading != 1 || seen.cells != 4 || seen.task != 1) return 4;
39+
40+
std::println("md4c 0.5.3 through import mcpplibs.mwrap.md4c: {} bytes of HTML, {} table cells", html.size(), seen.cells);
41+
return 0;
42+
}

0 commit comments

Comments
 (0)