• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tstack / lnav / 18478704608-2574

13 Oct 2025 09:28PM UTC coverage: 70.045% (-0.07%) from 70.119%
18478704608-2574

push

github

tstack
[md4c] add HTML generator

A bunch of external-access stuff.

69 of 167 new or added lines in 18 files covered. (41.32%)

1 existing line in 1 file now uncovered.

50324 of 71845 relevant lines covered (70.05%)

415329.15 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.21
/src/md4cpp.cc
1
/**
2
 * Copyright (c) 2022, Timothy Stack
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 * * Redistributions of source code must retain the above copyright notice, this
10
 * list of conditions and the following disclaimer.
11
 * * Redistributions in binary form must reproduce the above copyright notice,
12
 * this list of conditions and the following disclaimer in the documentation
13
 * and/or other materials provided with the distribution.
14
 * * Neither the name of Timothy Stack nor the names of its contributors
15
 * may be used to endorse or promote products derived from this software
16
 * without specific prior written permission.
17
 *
18
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
19
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
22
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
 */
29

30
#include "md4cpp.hh"
31

32
#include "base/is_utf8.hh"
33
#include "base/lnav_log.hh"
34
#include "emojis-json.h"
35
#include "pcrepp/pcre2pp.hh"
36
#include "xml-entities-json.h"
37
#include "yajlpp/yajlpp_def.hh"
38

39
namespace md4cpp {
40

41
static const typed_json_path_container<xml_entity_map>&
42
get_xml_entity_map_handlers()
35✔
43
{
44
    static const typed_json_path_container<xml_entity> xml_entity_handlers = {
45
        yajlpp::property_handler("characters").for_field(&xml_entity::xe_chars),
70✔
46
    };
140✔
47

48
    static const typed_json_path_container<xml_entity_map> retval = {
49
        yajlpp::pattern_property_handler("(?<var_name>\\&\\w+;?)")
×
50
            .with_synopsis("<name>")
35✔
51
            .with_path_provider<xml_entity_map>(
35✔
52
                [](struct xml_entity_map* xem,
×
53
                   std::vector<std::string>& paths_out) {
54
                    for (const auto& iter : xem->xem_entities) {
×
55
                        paths_out.emplace_back(iter.first);
×
56
                    }
57
                })
×
58
            .with_obj_provider<xml_entity, xml_entity_map>(
35✔
59
                [](const yajlpp_provider_context& ypc, xml_entity_map* xem) {
35✔
60
                    auto entity_name = ypc.get_substr(0);
468,510✔
61
                    return &xem->xem_entities[entity_name];
937,020✔
62
                })
468,510✔
63
            .with_children(xml_entity_handlers),
35✔
64
    };
140✔
65

66
    return retval;
35✔
67
}
140✔
68

69
static const typed_json_path_container<emoji_map>&
70
get_emoji_map_handlers()
38✔
71
{
72
    static const typed_json_path_container<emoji> emoji_handlers = {
73
        yajlpp::property_handler("emoji").for_field(&emoji::e_value),
76✔
74
        yajlpp::property_handler("shortname").for_field(&emoji::e_shortname),
76✔
75
    };
190✔
76

77
    static const typed_json_path_container<emoji_map> retval = {
78
        yajlpp::property_handler("emojis#")
76✔
79
            .for_field(&emoji_map::em_emojis)
38✔
80
            .with_children(emoji_handlers),
38✔
81
    };
152✔
82

83
    return retval;
38✔
84
}
190✔
85

86
static xml_entity_map
87
load_xml_entity_map()
35✔
88
{
89
    static const intern_string_t name
90
        = intern_string::lookup(xml_entities_json.get_name());
35✔
91
    auto sfp = xml_entities_json.to_string_fragment_producer();
35✔
92
    auto parse_res = get_xml_entity_map_handlers()
35✔
93
                         .parser_for(name)
70✔
94
                         .with_ignore_unused(true)
35✔
95
                         .of(*sfp);
35✔
96

97
    assert(parse_res.isOk());
98

99
    return parse_res.unwrap();
70✔
100
}
35✔
101

102
const xml_entity_map&
103
get_xml_entity_map()
35✔
104
{
105
    static const auto retval = load_xml_entity_map();
35✔
106

107
    return retval;
35✔
108
}
109

110
static emoji_map
111
load_emoji_map()
38✔
112
{
113
    static const intern_string_t name
114
        = intern_string::lookup(emojis_json.get_name());
38✔
115
    auto sfp = emojis_json.to_string_fragment_producer();
38✔
116
    auto parse_res
117
        = get_emoji_map_handlers().parser_for(name).with_ignore_unused(true).of(
76✔
118
            *sfp);
38✔
119

120
    assert(parse_res.isOk());
121

122
    auto retval = parse_res.unwrap();
38✔
123
    for (auto& em : retval.em_emojis) {
153,254✔
124
        retval.em_shortname2emoji.emplace(em.e_shortname, em);
153,216✔
125
    }
126

127
    return retval;
76✔
128
}
38✔
129

130
const emoji_map&
131
get_emoji_map()
159✔
132
{
133
    static const auto retval = load_emoji_map();
159✔
134

135
    return retval;
159✔
136
}
137

138
std::string
139
escape_html(string_fragment content)
×
140
{
141
    std::string retval;
×
142

143
    retval.reserve(content.length());
×
144
    for (const auto ch : content) {
×
145
        switch (ch) {
×
146
            case '"':
×
147
                retval.append("&quot;");
×
148
                break;
×
149
            case '\'':
×
150
                retval.append("&apos;");
×
151
                break;
×
152
            case '<':
×
153
                retval.append("&lt;");
×
154
                break;
×
155
            case '>':
×
156
                retval.append("&gt;");
×
157
                break;
×
158
            case '&':
×
159
                retval.append("&amp;");
×
160
                break;
×
161
            default:
×
162
                retval.push_back(ch);
×
163
                break;
×
164
        }
165
    }
166

167
    return retval;
×
168
}
×
169

170
file
171
parse_file(const std::filesystem::path& src, const string_fragment& sf)
12✔
172
{
173
    static const auto FRONTMATTER_RE = lnav::pcre2pp::code::from_const(
174
        R"((?:^---\n(.*)\n---\n|^\+\+\+\n(.*)\n\+\+\+\n))",
175
        PCRE2_MULTILINE | PCRE2_DOTALL);
12✔
176
    thread_local auto md = FRONTMATTER_RE.create_match_data();
12✔
177

178
    auto frontmatter_sf = string_fragment{};
12✔
179
    auto frontmatter_format = text_format_t::TF_UNKNOWN;
12✔
180
    auto content_sf = sf;
12✔
181

182
    auto cap_res = FRONTMATTER_RE.capture_from(content_sf)
12✔
183
                       .into(md)
12✔
184
                       .matches()
24✔
185
                       .ignore_error();
12✔
186
    if (cap_res) {
12✔
NEW
187
        if (md[1]) {
×
NEW
188
            frontmatter_format = text_format_t::TF_YAML;
×
NEW
189
            frontmatter_sf = md[1].value();
×
NEW
190
        } else if (md[2]) {
×
NEW
191
            frontmatter_format = text_format_t::TF_TOML;
×
NEW
192
            frontmatter_sf = md[2].value();
×
193
        }
NEW
194
        content_sf = cap_res->f_remaining;
×
195
    } else if (content_sf.startswith("{")) {
12✔
196
        yajlpp_parse_context ypc(intern_string::lookup(src));
4✔
197
        auto handle = yajlpp::alloc_handle(&ypc.ypc_callbacks, &ypc);
4✔
198

199
        yajl_config(handle.in(), yajl_allow_trailing_garbage, 1);
4✔
200
        ypc.with_ignore_unused(true)
4✔
201
            .with_handle(handle.in())
4✔
202
            .with_error_reporter([&src](const auto& ypc, const auto& um) {
4✔
NEW
203
                log_error(
×
204
                    "%s: failed to parse JSON front matter "
205
                    "-- %s",
206
                    src.c_str(),
207
                    um.um_reason.al_string.c_str());
NEW
208
            });
×
209
        if (ypc.parse_doc(content_sf)) {
4✔
210
            ssize_t consumed = ypc.ypc_total_consumed;
4✔
211
            if (consumed < content_sf.length() && content_sf[consumed] == '\n')
4✔
212
            {
213
                frontmatter_format = text_format_t::TF_JSON;
4✔
214
                frontmatter_sf = sf.sub_range(0, consumed);
4✔
215
                content_sf = content_sf.substr(consumed);
4✔
216
            }
217
        }
218
    }
4✔
219

220
    return {
221
        frontmatter_sf,
222
        frontmatter_format,
223
        content_sf,
224
    };
12✔
225
}
226

227
struct parse_userdata {
228
    event_handler& pu_handler;
229
    std::string pu_error_msg;
230
};
231

232
void
233
event_handler::set_line_number_from(const char* text)
276✔
234
{
235
    if (this->eh_fragment.begin() <= text && text < this->eh_fragment.end()) {
276✔
236
        auto off = text - this->eh_fragment.begin();
168✔
237

238
        this->eh_tree->visit_overlapping(
168✔
239
            off, off + 1, [this](const auto& cintv) {
168✔
240
                this->eh_line_number = cintv.value;
184✔
241
            });
184✔
242
    }
243
}
276✔
244

245
event_handler::block
246
event_handler::build_block(MD_BLOCKTYPE type, void* detail)
5,938✔
247
{
248
    switch (type) {
5,938✔
249
        case MD_BLOCK_DOC:
220✔
250
            return block_doc{};
220✔
251
        case MD_BLOCK_QUOTE:
70✔
252
            return block_quote{};
70✔
253
        case MD_BLOCK_UL:
182✔
254
            return static_cast<MD_BLOCK_UL_DETAIL*>(detail);
182✔
255
        case MD_BLOCK_OL:
6✔
256
            return static_cast<MD_BLOCK_OL_DETAIL*>(detail);
6✔
257
        case MD_BLOCK_LI:
716✔
258
            return static_cast<MD_BLOCK_LI_DETAIL*>(detail);
716✔
259
        case MD_BLOCK_HR:
×
260
            return block_hr{};
×
261
        case MD_BLOCK_H:
424✔
262
            return static_cast<MD_BLOCK_H_DETAIL*>(detail);
424✔
263
        case MD_BLOCK_CODE: {
276✔
264
            auto retval = static_cast<MD_BLOCK_CODE_DETAIL*>(detail);
276✔
265

266
            this->set_line_number_from(retval->lang.text);
276✔
267
            return retval;
276✔
268
        }
269
        case MD_BLOCK_HTML:
64✔
270
            return block_html{};
64✔
271
        case MD_BLOCK_P:
1,230✔
272
            return block_p{};
1,230✔
273
        case MD_BLOCK_TABLE:
100✔
274
            return static_cast<MD_BLOCK_TABLE_DETAIL*>(detail);
100✔
275
        case MD_BLOCK_THEAD:
100✔
276
            return block_thead{};
100✔
277
        case MD_BLOCK_TBODY:
100✔
278
            return block_tbody{};
100✔
279
        case MD_BLOCK_TR:
798✔
280
            return block_tr{};
798✔
281
        case MD_BLOCK_TH:
208✔
282
            return block_th{};
208✔
283
        case MD_BLOCK_TD:
1,444✔
284
            return static_cast<MD_BLOCK_TD_DETAIL*>(detail);
1,444✔
285
    }
286

287
    return {};
×
288
}
289

290
event_handler::span
291
event_handler::build_span(MD_SPANTYPE type, void* detail)
1,758✔
292
{
293
    switch (type) {
1,758✔
294
        case MD_SPAN_EM:
8✔
295
            return span_em{};
8✔
296
        case MD_SPAN_STRONG:
180✔
297
            return span_strong{};
180✔
298
        case MD_SPAN_A:
546✔
299
            return static_cast<MD_SPAN_A_DETAIL*>(detail);
546✔
300
        case MD_SPAN_IMG:
96✔
301
            return static_cast<MD_SPAN_IMG_DETAIL*>(detail);
96✔
302
        case MD_SPAN_CODE:
884✔
303
            return span_code{};
884✔
304
        case MD_SPAN_DEL:
8✔
305
            return span_del{};
8✔
306
        case MD_SPAN_U:
36✔
307
            return span_u{};
36✔
308
        default:
×
309
            break;
×
310
    }
311

312
    return {};
×
313
}
314

315
static int
316
md4cpp_enter_block(MD_BLOCKTYPE type, void* detail, void* userdata)
2,969✔
317
{
318
    auto* pu = static_cast<parse_userdata*>(userdata);
2,969✔
319

320
    auto enter_res
321
        = pu->pu_handler.enter_block(pu->pu_handler.build_block(type, detail));
2,969✔
322
    if (enter_res.isErr()) {
2,969✔
323
        pu->pu_error_msg = enter_res.unwrapErr();
×
324
        return 1;
×
325
    }
326

327
    return 0;
2,969✔
328
}
2,969✔
329

330
static int
331
md4cpp_leave_block(MD_BLOCKTYPE type, void* detail, void* userdata)
2,969✔
332
{
333
    auto* pu = static_cast<parse_userdata*>(userdata);
2,969✔
334
    auto leave_res
335
        = pu->pu_handler.leave_block(pu->pu_handler.build_block(type, detail));
2,969✔
336
    if (leave_res.isErr()) {
2,969✔
337
        pu->pu_error_msg = leave_res.unwrapErr();
×
338
        return 1;
×
339
    }
340

341
    return 0;
2,969✔
342
}
2,969✔
343

344
static int
345
md4cpp_enter_span(MD_SPANTYPE type, void* detail, void* userdata)
879✔
346
{
347
    auto* pu = static_cast<parse_userdata*>(userdata);
879✔
348

349
    auto enter_res
350
        = pu->pu_handler.enter_span(pu->pu_handler.build_span(type, detail));
879✔
351
    if (enter_res.isErr()) {
879✔
352
        pu->pu_error_msg = enter_res.unwrapErr();
×
353
        return 1;
×
354
    }
355

356
    return 0;
879✔
357
}
879✔
358

359
static int
360
md4cpp_leave_span(MD_SPANTYPE type, void* detail, void* userdata)
879✔
361
{
362
    auto* pu = static_cast<parse_userdata*>(userdata);
879✔
363

364
    auto leave_res
365
        = pu->pu_handler.leave_span(pu->pu_handler.build_span(type, detail));
879✔
366
    if (leave_res.isErr()) {
879✔
367
        pu->pu_error_msg = leave_res.unwrapErr();
×
368
        return 1;
×
369
    }
370

371
    return 0;
879✔
372
}
879✔
373

374
static int
375
md4cpp_text(MD_TEXTTYPE type, const MD_CHAR* text, MD_SIZE size, void* userdata)
5,892✔
376
{
377
    auto* pu = static_cast<parse_userdata*>(userdata);
5,892✔
378

379
    auto text_res = pu->pu_handler.text(type, string_fragment(text, 0, size));
5,892✔
380
    if (text_res.isErr()) {
5,892✔
381
        pu->pu_error_msg = text_res.unwrapErr();
×
382
        return 1;
×
383
    }
384

385
    return 0;
5,892✔
386
}
5,892✔
387

388
namespace details {
389
Result<void, std::string>
390
parse(const string_fragment& sf, event_handler& eh)
110✔
391
{
392
    auto scan_res = is_utf8(sf);
110✔
393
    if (!scan_res.is_valid()) {
110✔
394
        return Err(
×
395
            fmt::format(FMT_STRING("file has invalid UTF-8 at offset {}: {}"),
×
396
                        scan_res.usr_valid_frag.sf_end,
397
                        scan_res.usr_message));
×
398
    }
399

400
    auto eols = std::vector<event_handler::line_type_t>{};
110✔
401
    int lineno = 1;
110✔
402

403
    auto rest_sf = sf;
110✔
404
    while (!rest_sf.empty()) {
4,244✔
405
        auto split_pair = rest_sf.split_when(string_fragment::tag1{'\n'});
4,134✔
406
        auto line_sf = split_pair.first;
4,134✔
407

408
        eols.emplace_back(line_sf.sf_begin, line_sf.sf_end, lineno++);
4,134✔
409
        rest_sf = split_pair.second;
4,134✔
410
    }
411

412
    MD_PARSER parser = {0};
110✔
413
    eh.eh_fragment = sf;
110✔
414
    eh.eh_tree = std::make_unique<event_handler::lines_tree_t>(std::move(eols));
110✔
415
    auto pu = parse_userdata{eh};
110✔
416

417
    parser.abi_version = 0;
110✔
418
    parser.flags
419
        = (MD_DIALECT_GITHUB | MD_FLAG_UNDERLINE | MD_FLAG_STRIKETHROUGH)
110✔
420
        & ~(MD_FLAG_PERMISSIVEAUTOLINKS);
421
    parser.enter_block = md4cpp_enter_block;
110✔
422
    parser.leave_block = md4cpp_leave_block;
110✔
423
    parser.enter_span = md4cpp_enter_span;
110✔
424
    parser.leave_span = md4cpp_leave_span;
110✔
425
    parser.text = md4cpp_text;
110✔
426

427
    auto rc = md_parse(sf.data(), sf.length(), &parser, &pu);
110✔
428

429
    if (rc == 0) {
110✔
430
        return Ok();
110✔
431
    }
432

433
    return Err(pu.pu_error_msg);
×
434
}
110✔
435
}  // namespace details
436

437
}  // namespace md4cpp
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc