Files
pyxis/tests/native/test_nomadnet_image_parse.cpp
Torlando 929fa2c7ab feat(nomadnet): inline page images — parser, WebP decode, /media transport, LVGL render
Cumulative candidate on top of origin/main (7ae47d5).
Slices 1-5: compact-model image records, vendored decode-only libwebp,
wire contract + loader state machine, owner-loop /media dispatch with
retained page-Link reuse, screen render with bounded PSRAM LRU pixel store.

Local checkpoint only — not pushed.
2026-09-16 18:11:18 +00:00

217 lines
8.3 KiB
C++

// Host test: micron image-tag parsing in the Pyxis NomadNet DocumentParser.
// Reference semantics are pinned to markqvist/NomadNet e1e8ab8 (v1.4.3):
// - dispatch on a source line beginning with "`(" (MicronParser.py:550-551),
// calling parse_image(line[2:]) — i.e. the tag is "`(" alt "` props "` url ")".
// - parse_image() (MicronParser.py:221): rfind(")"), split on backticks,
// fields[0]=alt (stripped), fields[-1]=url (stripped), middle fields are
// key=value props; unknown props ignored; a/l/c align shorthands expand.
// - len(fields) < 2 or no closing ")" => not an image (reference returns
// None and the line is rendered as ordinary inline text).
// - The escape strip (leading backslash) and comment check run BEFORE image
// dispatch, so an escaped "`(" line is still an image.
// - Non-rendering clients display the alt-text placeholder.
#include "NomadNetDocument.h"
#include <cstdio>
#include <string>
using namespace UI::LXMF;
using namespace NomadNet;
static int failures = 0;
#define CHECK(cond) do { if (!(cond)) { std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); ++failures; } } while (0)
static bool image_block_at(const Document& doc, std::size_t index,
std::size_t* image_index) {
if (index >= doc.blocks.size()) return false;
if (doc.blocks[index].type != BlockType::IMAGE) return false;
if (doc.blocks[index].image_index < 0) return false;
if (image_index) *image_index = doc.blocks[index].image_index;
return *image_index < doc.images.size();
}
static std::string image_alt(const Document& doc, std::size_t image) {
return doc.images[image].alt;
}
static std::string image_url(const Document& doc, std::size_t image) {
return doc.images[image].url;
}
static bool any_image(const Document& doc) {
for (const auto& block : doc.blocks)
if (block.type == BlockType::IMAGE) return true;
return false;
}
int main() {
DocumentParser parser;
// 1. Canonical upstream example (Guide.py:1409).
{
const Document doc = parser.parse(
"`(The RNS logo`w=n`a=c`:/media/demo.webp)");
std::size_t image = 0;
CHECK(doc.blocks.size() == 1);
CHECK(image_block_at(doc, 0, &image));
// The url is stored verbatim, including its leading ":" (local-media
// marker resolved at transport time by url_delegate.resolve_image).
CHECK(image_url(doc, image) == ":/media/demo.webp");
CHECK(image_alt(doc, image) == "The RNS logo");
CHECK(doc.images[image].align == Alignment::CENTER);
CHECK(doc.images[image].width.kind == ImageDimension::NATIVE);
// Height was not specified: NONE (the reference leaves it None, i.e.
// auto), not NATIVE.
CHECK(doc.images[image].height.kind == ImageDimension::NONE);
}
// 2. Minimal form: alt and url only, no props.
{
const Document doc = parser.parse("`(/pic`:/pic)");
std::size_t image = 0;
CHECK(image_block_at(doc, 0, &image));
CHECK(image_url(doc, image) == ":/pic");
CHECK(image_alt(doc, image) == "/pic");
CHECK(doc.images[image].width.kind == ImageDimension::NONE);
CHECK(doc.images[image].align == Alignment::LEFT);
}
// 3. Alignment shorthands: l, r, c expand to left/right/center. One
// image per line (the reference is block-level, one tag per line).
{
const Document doc = parser.parse(
"`(a`a=l`:/a)\n`(b`a=c`:/b)\n`(c`a=r`:/c)");
std::size_t i0 = 0, i1 = 0, i2 = 0;
CHECK(image_block_at(doc, 0, &i0));
CHECK(image_block_at(doc, 1, &i1));
CHECK(image_block_at(doc, 2, &i2));
CHECK(doc.images[i0].align == Alignment::LEFT);
CHECK(doc.images[i1].align == Alignment::CENTER);
CHECK(doc.images[i2].align == Alignment::RIGHT);
}
// 4. Unknown properties are ignored; malformed key=value ignored too.
{
const Document doc = parser.parse(
"`(art`q=99`x`a=c`:/x)");
std::size_t image = 0;
CHECK(image_block_at(doc, 0, &image));
CHECK(image_alt(doc, image) == "art");
CHECK(image_url(doc, image) == ":/x");
CHECK(doc.images[image].align == Alignment::CENTER);
CHECK(doc.images[image].width.kind == ImageDimension::NONE);
}
// 5. Width/height property parsing: pixel budget, percent, native.
{
const Document doc = parser.parse(
"`(p`w=100`h=50`:/a.webp)\n`(n`w=n`h=n`:/b.webp)\n`(pc`w=50%`h=25%`:/c.webp)");
std::size_t i0 = 0, i1 = 0, i2 = 0;
CHECK(image_block_at(doc, 0, &i0));
CHECK(image_block_at(doc, 1, &i1));
CHECK(image_block_at(doc, 2, &i2));
CHECK(doc.images[i0].width.kind == ImageDimension::PIXELS &&
doc.images[i0].width.value == 100);
CHECK(doc.images[i0].height.kind == ImageDimension::PIXELS &&
doc.images[i0].height.value == 50);
CHECK(doc.images[i1].width.kind == ImageDimension::NATIVE);
CHECK(doc.images[i2].width.kind == ImageDimension::PERCENT &&
doc.images[i2].width.value == 50);
}
// 5b. Pixel budget over the cap is clamped, not dropped.
{
const Document doc = parser.parse("`(big`w=99999`:/big.webp)`");
std::size_t image = 0;
CHECK(image_block_at(doc, 0, &image));
CHECK(doc.images[image].width.kind == ImageDimension::PIXELS);
CHECK(doc.images[image].width.value ==
DocumentParser::MAX_IMAGE_PIXEL_BUDGET);
}
// 6. Not an image: no closing paren -> reference returns None and the
// line is rendered as ordinary inline text.
{
const Document doc = parser.parse("`(/media/none)`");
CHECK(!any_image(doc));
bool found_text = false;
for (const auto& block : doc.blocks)
for (const auto& run : block.runs)
if (run.text.find("/media/none") != std::string::npos)
found_text = true;
CHECK(found_text);
}
// 7. Not an image: fewer than two backtick fields (no url).
{
const Document doc = parser.parse("`(");
CHECK(!any_image(doc));
}
// 8. Image following a section-marker line still parses.
{
const Document doc = parser.parse("<\n`(<img`a=c`:/deep)`");
bool found = false;
std::size_t image = 0;
for (std::size_t i = 0; i < doc.blocks.size(); ++i)
if (image_block_at(doc, i, &image)) { found = true; break; }
CHECK(found);
CHECK(image_url(doc, image) == ":/deep");
}
// 9. Literal mode: "`(" is ordinary text, never an image.
{
const Document doc = parser.parse("`=\n`(/media/lit)\n`=");
CHECK(!any_image(doc));
}
// 10. Escaped line: the reference strips the leading backslash BEFORE
// image dispatch, so this is still an image.
{
const Document doc = parser.parse("\\`(/media/esc`:/x)`");
std::size_t image = 0;
CHECK(image_block_at(doc, 0, &image));
CHECK(image_url(doc, image) == ":/x");
}
// 11. Bounded: more than MAX_IMAGES images truncates, earlier ones
// retained.
{
std::string source;
for (int i = 0; i < static_cast<int>(DocumentParser::MAX_IMAGES) + 4; ++i) {
source += "`(img";
source += std::to_string(i);
source += "`:/m";
source += std::to_string(i);
source += ".webp)\n";
}
Document doc = parser.parse(source);
CHECK(doc.images.size() == DocumentParser::MAX_IMAGES);
CHECK(doc.truncated);
CHECK(doc.has_truncation(TruncationReason::IMAGES));
}
// 12. Overlong url is not admitted as an image (bounded field).
{
std::string url(400, '/');
const Document doc = parser.parse("`(/u`/" + url + ")");
CHECK(!any_image(doc));
CHECK(doc.truncated);
CHECK(doc.has_truncation(TruncationReason::IMAGE_URL_BYTES));
}
// 13. Overlong alt is retained truncated, not dropped.
{
std::string alt(300, 'A');
const Document doc = parser.parse("`(" + alt + "`:/ok)");
std::size_t image = 0;
CHECK(image_block_at(doc, 0, &image));
CHECK(image_alt(doc, image).size() ==
DocumentParser::MAX_IMAGE_ALT_BYTES);
}
if (failures == 0) std::printf("ALL IMAGE PARSER TESTS PASSED\n");
return failures == 0 ? 0 : 1;
}