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

mattwparas / steel / 18461079395

13 Oct 2025 09:20AM UTC coverage: 42.731% (-0.9%) from 43.668%
18461079395

Pull #536

github

web-flow
Merge 6f55a8b56 into e378cba22
Pull Request #536: Initial proposal for no_std support

63 of 755 new or added lines in 38 files covered. (8.34%)

73 existing lines in 15 files now uncovered.

12324 of 28841 relevant lines covered (42.73%)

3215759.81 hits per line

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

0.0
/libs/steel-markdown/src/lib.rs
1
use steel::{
2
    gc::Shared,
3
    rvals::Custom,
4
    steel_vm::ffi::{FFIModule, FFIValue, IntoFFIVal, RegisterFFIFn},
5
};
6

7
use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag, TagEnd};
8

9
use syntect::highlighting::ThemeSet;
10
use syntect::{html::highlighted_html_for_string, parsing::SyntaxSet};
11

12
// fn main() {
13
// let ss = SyntaxSet::load_defaults_newlines();
14
// let ts = ThemeSet::load_defaults();
15

16
// let args: Vec<String> = std::env::args().collect();
17
// if args.len() < 2 {
18
//     println!("Please pass in a file to highlight");
19
//     return;
20
// }
21

22
// let style = "
23
//     pre {
24
//         font-size:13px;
25
//         font-family: Consolas, \"Liberation Mono\", Menlo, Courier, monospace;
26
//     }";
27
// println!("<head><title>{}</title><style>{}</style></head>", &args[1], style);
28
// let theme = &ts.themes["base16-ocean.dark"];
29
// let c = theme.settings.background.unwrap_or(Color::WHITE);
30
// println!("<body style=\"background-color:#{:02x}{:02x}{:02x};\">\n", c.r, c.g, c.b);
31
// let html = highlighted_html_for_file(&args[1], &ss, theme).unwrap();
32
// println!("{}", html);
33
// println!("</body>");
34
// }
35

36
struct SyntaxHighlighter {
37
    ss: SyntaxSet,
38
    ts: ThemeSet,
39
}
40

41
impl Custom for SyntaxHighlighter {}
42

43
#[derive(Debug)]
44
struct SyntectError {
45
    _inner: syntect::Error,
46
}
47
impl Custom for SyntectError {}
48

49
impl SyntaxHighlighter {
50
    pub fn new() -> Self {
×
51
        Self {
52
            ss: SyntaxSet::load_defaults_newlines(),
×
53
            ts: ThemeSet::load_defaults(),
×
54
        }
55
    }
56

57
    pub fn highlight_text(&self, language: String, input: String) -> Result<String, SyntectError> {
×
58
        // dbg!(self.ss.syntaxes());
59

60
        // for s in self.ss.syntaxes() {
61
        //     println!("{}", s.name);
62
        // }
63

64
        let reference = self.ss.find_syntax_by_name(&language).unwrap();
×
65

66
        highlighted_html_for_string(
67
            &input,
×
68
            &self.ss,
×
69
            reference,
×
70
            &self.ts.themes["base16-ocean.dark"],
×
71
        )
NEW
72
        .map_err(|e| SyntectError { _inner: e })
×
73
    }
74
}
75

76
pub struct MarkdownEvent {
77
    event: Event<'static>,
78
    source: Shared<str>,
79
}
80

81
pub struct MarkdownTag {
82
    tag: Tag<'static>,
83
    source: Shared<str>,
84
}
85

86
pub struct MarkdownEndTag {
87
    _tag: TagEnd,
88
    _source: Shared<str>,
89
}
90

91
impl Custom for MarkdownTag {}
92
impl Custom for MarkdownEndTag {}
93
impl Custom for MarkdownEvent {}
94

95
impl MarkdownEvent {
96
    // Convert this one event to html
97
    fn to_html(&self) -> String {
×
98
        let mut output = String::new();
×
99

100
        pulldown_cmark::html::push_html(&mut output, std::iter::once(self.event.clone()));
×
101

102
        output
×
103
    }
104

105
    fn is_text(&self) -> bool {
×
106
        matches!(self.event, Event::Text(_))
×
107
    }
108

109
    fn is_code(&self) -> bool {
×
110
        matches!(self.event, Event::Code(_))
×
111
    }
112

113
    fn set_code(&mut self, input: String) -> bool {
×
114
        if let Event::Code(ref mut text) = &mut self.event {
×
115
            *text = input.into();
116
            true
117
        } else {
118
            false
×
119
        }
120
    }
121

122
    fn is_html(&self) -> bool {
×
123
        matches!(self.event, Event::Html(_))
×
124
    }
125

126
    fn is_start(&self) -> bool {
×
127
        matches!(self.event, Event::Start(_))
×
128
    }
129

130
    fn is_end(&self) -> bool {
×
131
        matches!(self.event, Event::End(_))
×
132
    }
133

134
    fn set_text(&mut self, text: String) -> bool {
×
135
        if let Event::Text(t) = &mut self.event {
×
136
            *t = CowStr::from(text);
137
            true
138
        } else {
139
            false
×
140
        }
141
    }
142

143
    fn set_event_as_html(&mut self, html_text: String) {
×
144
        self.event = Event::Html(html_text.into());
×
145
    }
146

147
    fn as_text(&self) -> Option<String> {
×
148
        if let Event::Text(t) = &self.event {
×
149
            Some(t.to_string())
150
        } else {
151
            None
×
152
        }
153
    }
154

155
    fn as_code(&self) -> Option<String> {
×
156
        if let Event::Code(t) = &self.event {
×
157
            Some(t.to_string())
158
        } else {
159
            None
×
160
        }
161
    }
162

163
    fn as_html(&self) -> Option<String> {
×
164
        if let Event::Code(t) = &self.event {
×
165
            Some(t.to_string())
166
        } else {
167
            None
×
168
        }
169
    }
170

171
    fn as_start_tag(&self) -> Option<FFIValue> {
×
172
        if let Event::Start(tag) = self.event.clone() {
×
173
            Some(
174
                MarkdownTag {
175
                    tag,
176
                    source: self.source.clone(),
177
                }
178
                .into_ffi_val()
179
                .unwrap(),
180
            )
181
        } else {
182
            None
×
183
        }
184
    }
185

186
    fn as_end_tag(&self) -> Option<FFIValue> {
×
187
        if let Event::End(tag) = self.event.clone() {
×
188
            Some(
189
                MarkdownEndTag { _tag: tag, _source: self.source.clone() }
190
                .into_ffi_val()
191
                .unwrap(),
192
            )
193
        } else {
194
            None
×
195
        }
196
    }
197
}
198

199
struct MarkdownCodeBlockKind {
200
    _source: Shared<str>,
201
    kind: CodeBlockKind<'static>,
202
}
203

204
impl MarkdownCodeBlockKind {
205
    fn is_indented(&self) -> bool {
×
206
        matches!(self.kind, CodeBlockKind::Indented)
×
207
    }
208

209
    fn is_fenced(&self) -> bool {
×
210
        matches!(self.kind, CodeBlockKind::Fenced(..))
×
211
    }
212

213
    fn as_fenced(&self) -> Option<String> {
×
214
        if let CodeBlockKind::Fenced(label) = &self.kind {
×
215
            Some(label.to_string())
216
        } else {
217
            None
×
218
        }
219
    }
220
}
221

222
impl Custom for MarkdownCodeBlockKind {}
223

224
impl MarkdownTag {
225
    fn is_paragraph(&self) -> bool {
×
226
        matches!(self.tag, Tag::Paragraph)
×
227
    }
228

229
    fn is_heading(&self) -> bool {
×
230
        matches!(self.tag, Tag::Heading { .. })
×
231
    }
232

233
    fn is_block_quote(&self) -> bool {
×
234
        matches!(self.tag, Tag::BlockQuote(..))
×
235
    }
236

237
    fn is_code_block(&self) -> bool {
×
238
        matches!(self.tag, Tag::CodeBlock(..))
×
239
    }
240

241
    fn as_heading(&self) -> Option<FFIValue> {
×
242
        if let Tag::Heading {
243
            level, id, classes, ..
×
244
        } = &self.tag
×
245
        {
246
            Some(
247
                vec![
248
                    (*level as isize).into_ffi_val().unwrap(),
249
                    id.as_ref().map(|x| x.to_string()).into_ffi_val().unwrap(),
×
250
                    classes
251
                        .iter()
252
                        .map(|x| x.to_string())
×
253
                        .collect::<Vec<_>>()
254
                        .into_ffi_val()
255
                        .unwrap(),
256
                ]
257
                .into_ffi_val()
258
                .unwrap(),
259
            )
260
        } else {
261
            None
×
262
        }
263
    }
264

265
    fn as_code_block(&self) -> Option<FFIValue> {
×
266
        if let Tag::CodeBlock(kind) = &self.tag {
×
267
            Some(
268
                MarkdownCodeBlockKind {
269
                    _source: self.source.clone(),
270
                    kind: kind.clone(),
271
                }
272
                .into_ffi_val()
273
                .unwrap(),
274
            )
275
        } else {
276
            None
×
277
        }
278
    }
279

280
    fn is_list(&self) -> bool {
×
281
        matches!(self.tag, Tag::List(..))
×
282
    }
283

284
    fn is_foot_note_definition(&self) -> bool {
×
285
        matches!(self.tag, Tag::FootnoteDefinition(_))
×
286
    }
287

288
    fn is_table(&self) -> bool {
×
289
        matches!(self.tag, Tag::Table(..))
×
290
    }
291

292
    fn is_table_head(&self) -> bool {
×
293
        matches!(self.tag, Tag::TableHead)
×
294
    }
295

296
    fn is_table_row(&self) -> bool {
×
297
        matches!(self.tag, Tag::TableRow)
×
298
    }
299

300
    fn is_table_cell(&self) -> bool {
×
301
        matches!(self.tag, Tag::TableCell)
×
302
    }
303

304
    fn is_emphasis(&self) -> bool {
×
305
        matches!(self.tag, Tag::Emphasis)
×
306
    }
307

308
    fn is_strong(&self) -> bool {
×
309
        matches!(self.tag, Tag::Strong)
×
310
    }
311

312
    fn is_strike_through(&self) -> bool {
×
313
        matches!(self.tag, Tag::Strikethrough)
×
314
    }
315

316
    fn is_link(&self) -> bool {
×
317
        matches!(self.tag, Tag::Link { .. })
×
318
    }
319

320
    fn is_image(&self) -> bool {
×
321
        matches!(self.tag, Tag::Image { .. })
×
322
    }
323
}
324

325
struct MarkdownParser {
326
    source: Shared<str>,
327
    parser: Parser<'static>,
328
}
329

330
impl MarkdownParser {
331
    fn new(source: String) -> Self {
×
332
        let source: Shared<str> = Shared::from(source);
×
333

334
        Self {
335
            source: source.clone(),
×
336
            // SAFETY: We're going to lift this to a static lifetime
337
            // by calling to owned on each of the nodes, and keeping
338
            // the source location that we're referencing around for
339
            // the same lifetime as this object.
340
            parser: unsafe {
×
341
                std::mem::transmute::<Parser<'_>, Parser<'static>>(Parser::new_ext(
342
                    &source,
343
                    Options::all(),
344
                ))
345
            },
346
        }
347
    }
348

349
    fn next(&mut self) -> Option<FFIValue> {
×
350
        self.parser.next().map(|event| {
×
351
            MarkdownEvent {
×
352
                event: event.to_owned(),
×
353
                source: self.source.clone(),
×
354
            }
355
            .into_ffi_val()
×
356
            .unwrap()
×
357
        })
358
    }
359
}
360

361
impl Custom for MarkdownParser {}
362

363
pub fn parse(input: String) -> Vec<FFIValue> {
×
364
    let input = Shared::from(input);
×
365

366
    Parser::new(&input)
×
367
        .map(|x| {
×
368
            // SAFETY: We're going to lift this to a static lifetime
369
            // by calling to owned on each of the nodes, and keeping
370
            // the source location that we're referencing around for
371
            // the same lifetime as this object.
372
            MarkdownEvent {
×
373
                event: unsafe { std::mem::transmute::<Event<'_>, Event<'static>>(x.to_owned()) },
×
374
                source: input.clone(),
×
375
            }
376
            .into_ffi_val()
×
377
            .unwrap()
×
378
        })
379
        .collect()
380
}
381

382
steel::declare_module!(build_module);
383

384
pub fn build_module() -> FFIModule {
×
385
    let mut module = FFIModule::new("dylib/steel/markdown");
×
386

387
    module
×
388
        .register_fn("parse", parse)
×
389
        .register_fn("parser", MarkdownParser::new)
×
390
        .register_fn("parser-next", MarkdownParser::next)
×
391
        .register_fn("event->html-string", MarkdownEvent::to_html)
×
392
        .register_fn("event-text?", MarkdownEvent::is_text)
×
393
        .register_fn("event-code?", MarkdownEvent::is_code)
×
394
        .register_fn("set-event-code!", MarkdownEvent::set_code)
×
395
        .register_fn("event-html?", MarkdownEvent::is_html)
×
396
        .register_fn("event-start?", MarkdownEvent::is_start)
×
397
        .register_fn("event-end?", MarkdownEvent::is_end)
×
398
        .register_fn("event->text", MarkdownEvent::as_text)
×
399
        .register_fn("set-event-text!", MarkdownEvent::set_text)
×
400
        .register_fn("set-event-as-html!", MarkdownEvent::set_event_as_html)
×
401
        .register_fn("event->code", MarkdownEvent::as_code)
×
402
        .register_fn("event->html", MarkdownEvent::as_html)
×
403
        .register_fn("event->start-tag", MarkdownEvent::as_start_tag)
×
404
        .register_fn("event->end-tag", MarkdownEvent::as_end_tag)
×
405
        .register_fn("tag-paragraph?", MarkdownTag::is_paragraph)
×
406
        .register_fn("tag-heading?", MarkdownTag::is_heading)
×
407
        .register_fn("tag-block-quote?", MarkdownTag::is_block_quote)
×
408
        .register_fn("tag-code-block?", MarkdownTag::is_code_block)
×
409
        .register_fn("tag-list?", MarkdownTag::is_list)
×
410
        .register_fn(
411
            "tag-foot-note-definition?",
412
            MarkdownTag::is_foot_note_definition,
×
413
        )
414
        .register_fn("tag-table?", MarkdownTag::is_table)
×
415
        .register_fn("tag-table-head?", MarkdownTag::is_table_head)
×
416
        .register_fn("tag-table-row?", MarkdownTag::is_table_row)
×
417
        .register_fn("tag-table-cell?", MarkdownTag::is_table_cell)
×
418
        .register_fn("tag-emphasis?", MarkdownTag::is_emphasis)
×
419
        .register_fn("tag-strong?", MarkdownTag::is_strong)
×
420
        .register_fn("tag-strike-through?", MarkdownTag::is_strike_through)
×
421
        .register_fn("tag-link?", MarkdownTag::is_link)
×
422
        .register_fn("tag-image?", MarkdownTag::is_image)
×
423
        .register_fn("tag->heading", MarkdownTag::as_heading)
×
424
        .register_fn("tag->code-block", MarkdownTag::as_code_block)
×
425
        .register_fn("code-block-indented?", MarkdownCodeBlockKind::is_indented)
×
426
        .register_fn("code-block-fenced?", MarkdownCodeBlockKind::is_fenced)
×
427
        .register_fn("code-block->fenced", MarkdownCodeBlockKind::as_fenced)
×
428
        .register_fn("event->string", |event: &MarkdownEvent| -> String {
×
429
            format!("{:?}", event.event)
×
430
        })
431
        .register_fn("syntax-highlighter", SyntaxHighlighter::new)
×
432
        .register_fn(
433
            "syntax-highlighter/text->highlighted-html",
434
            SyntaxHighlighter::highlight_text,
435
        );
436
    module
×
437
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc