carve-lang 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,508 @@
1
+ //! Serialize a carve-rs `Document` AST into a JSON string.
2
+ //!
3
+ //! The Ruby wrapper (`Carve.parse`) turns this JSON back into a tree of Ruby
4
+ //! Hashes/Arrays (`JSON.parse(..., symbolize_names: true)`), giving Ruby
5
+ //! consumers - notably a HexaPDF PDF renderer - a walkable AST without
6
+ //! reimplementing the parser or binding every struct as a Ruby class.
7
+ //!
8
+ //! Every node becomes a JSON object with a `"type"` tag plus its fields.
9
+ //! Inline text is `{"type":"text","value":"..."}`. Child collections are
10
+ //! JSON arrays. `Attrs` become `{"id","classes","key_values"}` or `null`.
11
+ //!
12
+ //! This walker lives in the binding (not carve-rs) so the engine stays free of
13
+ //! a serde dependency; only the gem's native extension pulls in `serde_json`.
14
+
15
+ use carve_rs::{
16
+ Abbreviation, AbbreviationDef, Admonition, Attrs, AutoLink, BlockExtension, BlockNode,
17
+ BlockQuote, CaptionNumber, Citation, CitationGroup, CitationRenderMode, CodeBlock, Comment,
18
+ CriticComment, CriticDelete, CriticInsert, CriticSubstitute, CrossRef, DefinitionList, Div,
19
+ Document, Emoji, Emphasis, EmphasisKind, Figure, FigureTarget, Footnote, Heading, Image,
20
+ InlineExtension, InlineNode, Link, List, ListItem, Math, Mention, OrderedListType, Paragraph,
21
+ RawBlock, RawInline, Span, Table, TableAlign, TableCell, TableCellSpan, TableRow, Tag,
22
+ ThematicBreak,
23
+ };
24
+ use serde_json::{Map, Value};
25
+
26
+ /// Public entry point: a full `Document` to a JSON string.
27
+ pub fn document_to_json(doc: &Document) -> String {
28
+ let mut m = Map::new();
29
+ m.insert("type".into(), "document".into());
30
+
31
+ let mut fm = Map::new();
32
+ for (k, v) in &doc.frontmatter {
33
+ fm.insert(k.clone(), Value::String(v.clone()));
34
+ }
35
+ m.insert("frontmatter".into(), Value::Object(fm));
36
+
37
+ let mut fdefs = Map::new();
38
+ for (k, v) in &doc.footnote_defs {
39
+ fdefs.insert(k.clone(), blocks(v));
40
+ }
41
+ m.insert("footnote_defs".into(), Value::Object(fdefs));
42
+
43
+ m.insert("children".into(), blocks(&doc.children));
44
+ m.insert("source_len".into(), Value::from(doc.source_len));
45
+
46
+ Value::Object(m).to_string()
47
+ }
48
+
49
+ // ---- helpers ---------------------------------------------------------------
50
+
51
+ fn obj(pairs: Vec<(&str, Value)>) -> Value {
52
+ let mut m = Map::new();
53
+ for (k, v) in pairs {
54
+ m.insert(k.into(), v);
55
+ }
56
+ Value::Object(m)
57
+ }
58
+
59
+ fn opt_str(o: &Option<String>) -> Value {
60
+ match o {
61
+ Some(s) => Value::String(s.clone()),
62
+ None => Value::Null,
63
+ }
64
+ }
65
+
66
+ fn opt_usize(o: &Option<usize>) -> Value {
67
+ match o {
68
+ Some(n) => Value::from(*n),
69
+ None => Value::Null,
70
+ }
71
+ }
72
+
73
+ fn attrs(a: &Option<Attrs>) -> Value {
74
+ match a {
75
+ None => Value::Null,
76
+ Some(at) => {
77
+ let mut kv = Map::new();
78
+ for (k, v) in &at.key_values {
79
+ kv.insert(k.clone(), Value::String(v.clone()));
80
+ }
81
+ obj(vec![
82
+ ("id", opt_str(&at.id)),
83
+ (
84
+ "classes",
85
+ Value::Array(at.classes.iter().map(|c| Value::String(c.clone())).collect()),
86
+ ),
87
+ ("key_values", Value::Object(kv)),
88
+ ])
89
+ }
90
+ }
91
+ }
92
+
93
+ fn opt_inlines(o: &Option<Vec<InlineNode>>) -> Value {
94
+ match o {
95
+ Some(v) => inlines(v),
96
+ None => Value::Null,
97
+ }
98
+ }
99
+
100
+ fn blocks(list: &[BlockNode]) -> Value {
101
+ Value::Array(list.iter().map(block).collect())
102
+ }
103
+
104
+ fn inlines(list: &[InlineNode]) -> Value {
105
+ Value::Array(list.iter().map(inline).collect())
106
+ }
107
+
108
+ // ---- block nodes -----------------------------------------------------------
109
+
110
+ fn block(b: &BlockNode) -> Value {
111
+ match b {
112
+ BlockNode::Heading(Heading { attrs: a, level, children }) => obj(vec![
113
+ ("type", "heading".into()),
114
+ ("level", Value::from(*level)),
115
+ ("children", inlines(children)),
116
+ ("attrs", attrs(a)),
117
+ ]),
118
+ BlockNode::Paragraph(Paragraph { attrs: a, children }) => obj(vec![
119
+ ("type", "paragraph".into()),
120
+ ("children", inlines(children)),
121
+ ("attrs", attrs(a)),
122
+ ]),
123
+ BlockNode::CodeBlock(c) => code_block(c),
124
+ BlockNode::List(List { attrs: a, ordered, start, ol_type, tight, items }) => obj(vec![
125
+ ("type", "list".into()),
126
+ ("ordered", Value::Bool(*ordered)),
127
+ ("start", opt_usize(start)),
128
+ ("ol_type", ol_type.map(ol_type_str).map(Value::from).unwrap_or(Value::Null)),
129
+ ("tight", Value::Bool(*tight)),
130
+ (
131
+ "items",
132
+ Value::Array(items.iter().map(list_item).collect()),
133
+ ),
134
+ ("attrs", attrs(a)),
135
+ ]),
136
+ BlockNode::BlockQuote(q) => block_quote(q),
137
+ BlockNode::Table(t) => table(t),
138
+ BlockNode::Admonition(Admonition { attrs: a, kind, title, label, children }) => obj(vec![
139
+ ("type", "admonition".into()),
140
+ ("kind", Value::String(kind.clone())),
141
+ ("title", opt_inlines(title)),
142
+ ("label", opt_str(label)),
143
+ ("children", blocks(children)),
144
+ ("attrs", attrs(a)),
145
+ ]),
146
+ BlockNode::Div(Div { attrs: a, label, children }) => obj(vec![
147
+ ("type", "div".into()),
148
+ ("label", opt_str(label)),
149
+ ("children", blocks(children)),
150
+ ("attrs", attrs(a)),
151
+ ]),
152
+ BlockNode::DefinitionList(DefinitionList { attrs: a, items }) => obj(vec![
153
+ ("type", "definition_list".into()),
154
+ (
155
+ "items",
156
+ Value::Array(
157
+ items
158
+ .iter()
159
+ .map(|it| {
160
+ obj(vec![
161
+ (
162
+ "terms",
163
+ Value::Array(it.terms.iter().map(|t| inlines(t)).collect()),
164
+ ),
165
+ (
166
+ "definitions",
167
+ Value::Array(
168
+ it.definitions.iter().map(|d| blocks(d)).collect(),
169
+ ),
170
+ ),
171
+ ])
172
+ })
173
+ .collect(),
174
+ ),
175
+ ),
176
+ ("attrs", attrs(a)),
177
+ ]),
178
+ BlockNode::Figure(Figure { attrs: a, target, caption }) => obj(vec![
179
+ ("type", "figure".into()),
180
+ ("target", figure_target(target)),
181
+ ("caption", inlines(caption)),
182
+ ("attrs", attrs(a)),
183
+ ]),
184
+ BlockNode::AbbreviationDef(AbbreviationDef { abbr, expansion }) => obj(vec![
185
+ ("type", "abbreviation_def".into()),
186
+ ("abbr", Value::String(abbr.clone())),
187
+ ("expansion", Value::String(expansion.clone())),
188
+ ]),
189
+ BlockNode::RawBlock(RawBlock { format, content }) => obj(vec![
190
+ ("type", "raw_block".into()),
191
+ ("format", Value::String(format.clone())),
192
+ ("content", Value::String(content.clone())),
193
+ ]),
194
+ BlockNode::Comment(Comment { block, content }) => obj(vec![
195
+ ("type", "comment".into()),
196
+ ("block", Value::Bool(*block)),
197
+ ("content", Value::String(content.clone())),
198
+ ]),
199
+ BlockNode::Extension(BlockExtension { attrs: a, name, children, summary, label }) => {
200
+ obj(vec![
201
+ ("type", "block_extension".into()),
202
+ ("name", Value::String(name.clone())),
203
+ ("children", blocks(children)),
204
+ ("summary", opt_str(summary)),
205
+ ("label", opt_str(label)),
206
+ ("attrs", attrs(a)),
207
+ ])
208
+ }
209
+ BlockNode::BlockImage(img) => {
210
+ // A block-level image reuses the inline Image shape but tagged
211
+ // block_image so a renderer can place it as a standalone figure.
212
+ let mut v = image(img);
213
+ if let Value::Object(ref mut m) = v {
214
+ m.insert("type".into(), "block_image".into());
215
+ }
216
+ v
217
+ }
218
+ BlockNode::ThematicBreak(ThematicBreak { attrs: a }) => obj(vec![
219
+ ("type", "thematic_break".into()),
220
+ ("attrs", attrs(a)),
221
+ ]),
222
+ }
223
+ }
224
+
225
+ fn code_block(c: &CodeBlock) -> Value {
226
+ obj(vec![
227
+ ("type", "code_block".into()),
228
+ ("lang", opt_str(&c.lang)),
229
+ ("title", opt_str(&c.title)),
230
+ ("label", opt_str(&c.label)),
231
+ ("content", Value::String(c.content.clone())),
232
+ ("attrs", attrs(&c.attrs)),
233
+ ])
234
+ }
235
+
236
+ fn block_quote(q: &BlockQuote) -> Value {
237
+ obj(vec![
238
+ ("type", "block_quote".into()),
239
+ ("children", blocks(&q.children)),
240
+ ("attribution", opt_inlines(&q.attribution)),
241
+ ("attrs", attrs(&q.attrs)),
242
+ ])
243
+ }
244
+
245
+ fn list_item(it: &ListItem) -> Value {
246
+ obj(vec![
247
+ ("type", "list_item".into()),
248
+ (
249
+ "checked",
250
+ match it.checked {
251
+ Some(b) => Value::Bool(b),
252
+ None => Value::Null,
253
+ },
254
+ ),
255
+ ("children", blocks(&it.children)),
256
+ ("attrs", attrs(&it.attrs)),
257
+ ])
258
+ }
259
+
260
+ fn table(t: &Table) -> Value {
261
+ obj(vec![
262
+ ("type", "table".into()),
263
+ ("caption", opt_inlines(&t.caption)),
264
+ (
265
+ "rows",
266
+ Value::Array(t.rows.iter().map(table_row).collect()),
267
+ ),
268
+ ("attrs", attrs(&t.attrs)),
269
+ ])
270
+ }
271
+
272
+ fn table_row(r: &TableRow) -> Value {
273
+ obj(vec![
274
+ ("type", "table_row".into()),
275
+ (
276
+ "cells",
277
+ Value::Array(r.cells.iter().map(table_cell).collect()),
278
+ ),
279
+ ("attrs", attrs(&r.attrs)),
280
+ ])
281
+ }
282
+
283
+ fn table_cell(c: &TableCell) -> Value {
284
+ obj(vec![
285
+ ("type", "table_cell".into()),
286
+ ("header", Value::Bool(c.header)),
287
+ (
288
+ "span",
289
+ match c.span {
290
+ Some(TableCellSpan::Rowspan) => "rowspan".into(),
291
+ Some(TableCellSpan::Colspan) => "colspan".into(),
292
+ None => Value::Null,
293
+ },
294
+ ),
295
+ (
296
+ "align",
297
+ match c.align {
298
+ Some(TableAlign::Left) => "left".into(),
299
+ Some(TableAlign::Right) => "right".into(),
300
+ Some(TableAlign::Center) => "center".into(),
301
+ None => Value::Null,
302
+ },
303
+ ),
304
+ ("children", inlines(&c.children)),
305
+ ("attrs", attrs(&c.attrs)),
306
+ ])
307
+ }
308
+
309
+ fn figure_target(t: &FigureTarget) -> Value {
310
+ match t {
311
+ FigureTarget::Image(img) => image(img),
312
+ FigureTarget::BlockQuote(q) => block_quote(q),
313
+ FigureTarget::Table(tb) => table(tb),
314
+ FigureTarget::CodeBlock(c) => code_block(c),
315
+ FigureTarget::Paragraph(Paragraph { attrs: a, children }) => obj(vec![
316
+ ("type", "paragraph".into()),
317
+ ("children", inlines(children)),
318
+ ("attrs", attrs(a)),
319
+ ]),
320
+ }
321
+ }
322
+
323
+ fn ol_type_str(t: OrderedListType) -> &'static str {
324
+ match t {
325
+ OrderedListType::LowerAlpha => "lower-alpha",
326
+ OrderedListType::UpperAlpha => "upper-alpha",
327
+ OrderedListType::LowerRoman => "lower-roman",
328
+ OrderedListType::UpperRoman => "upper-roman",
329
+ }
330
+ }
331
+
332
+ // ---- inline nodes ----------------------------------------------------------
333
+
334
+ fn inline(n: &InlineNode) -> Value {
335
+ match n {
336
+ InlineNode::Text(s) => obj(vec![
337
+ ("type", "text".into()),
338
+ ("value", Value::String(s.clone())),
339
+ ]),
340
+ InlineNode::Emphasis(Emphasis { attrs: a, kind, children }) => obj(vec![
341
+ ("type", "emphasis".into()),
342
+ ("kind", emphasis_kind_str(*kind).into()),
343
+ ("children", inlines(children)),
344
+ ("attrs", attrs(a)),
345
+ ]),
346
+ InlineNode::Code(s, a) => obj(vec![
347
+ ("type", "code".into()),
348
+ ("value", Value::String(s.clone())),
349
+ ("attrs", attrs(a)),
350
+ ]),
351
+ InlineNode::Link(l) => link(l),
352
+ InlineNode::Image(img) => image(img),
353
+ InlineNode::Span(Span { attrs: a, children }) => obj(vec![
354
+ ("type", "span".into()),
355
+ ("children", inlines(children)),
356
+ ("attrs", attrs(a)),
357
+ ]),
358
+ InlineNode::Math(Math { attrs: a, display, content }) => obj(vec![
359
+ ("type", "math".into()),
360
+ ("display", Value::Bool(*display)),
361
+ ("content", Value::String(content.clone())),
362
+ ("attrs", attrs(a)),
363
+ ]),
364
+ InlineNode::RawInline(RawInline { format, content }) => obj(vec![
365
+ ("type", "raw_inline".into()),
366
+ ("format", Value::String(format.clone())),
367
+ ("content", Value::String(content.clone())),
368
+ ]),
369
+ InlineNode::Emoji(Emoji { name }) => obj(vec![
370
+ ("type", "emoji".into()),
371
+ ("name", Value::String(name.clone())),
372
+ ]),
373
+ InlineNode::AutoLink(AutoLink { attrs: a, href, text }) => obj(vec![
374
+ ("type", "autolink".into()),
375
+ ("href", Value::String(href.clone())),
376
+ ("text", Value::String(text.clone())),
377
+ ("attrs", attrs(a)),
378
+ ]),
379
+ InlineNode::CrossRef(CrossRef { target }) => obj(vec![
380
+ ("type", "cross_ref".into()),
381
+ ("target", Value::String(target.clone())),
382
+ ]),
383
+ InlineNode::CaptionNumber(CaptionNumber { number }) => obj(vec![
384
+ ("type", "caption_number".into()),
385
+ ("number", opt_usize(number)),
386
+ ]),
387
+ InlineNode::Mention(Mention { user }) => obj(vec![
388
+ ("type", "mention".into()),
389
+ ("user", Value::String(user.clone())),
390
+ ]),
391
+ InlineNode::Tag(Tag { name }) => obj(vec![
392
+ ("type", "tag".into()),
393
+ ("name", Value::String(name.clone())),
394
+ ]),
395
+ InlineNode::CitationGroup(g) => citation_group(g),
396
+ InlineNode::Extension(InlineExtension { attrs: a, name, children }) => obj(vec![
397
+ ("type", "inline_extension".into()),
398
+ ("name", Value::String(name.clone())),
399
+ ("children", inlines(children)),
400
+ ("attrs", attrs(a)),
401
+ ]),
402
+ InlineNode::Abbreviation(Abbreviation { abbr, expansion }) => obj(vec![
403
+ ("type", "abbreviation".into()),
404
+ ("abbr", Value::String(abbr.clone())),
405
+ ("expansion", Value::String(expansion.clone())),
406
+ ]),
407
+ InlineNode::Footnote(Footnote { attrs: a, id, inline: inl, number, ref_id }) => obj(vec![
408
+ ("type", "footnote".into()),
409
+ ("id", opt_str(id)),
410
+ ("inline", opt_inlines(inl)),
411
+ ("number", opt_usize(number)),
412
+ ("ref_id", opt_str(ref_id)),
413
+ ("attrs", attrs(a)),
414
+ ]),
415
+ InlineNode::SoftBreak => obj(vec![("type", "soft_break".into())]),
416
+ InlineNode::HardBreak => obj(vec![("type", "hard_break".into())]),
417
+ InlineNode::CriticInsert(CriticInsert { attrs: a, children }) => obj(vec![
418
+ ("type", "critic_insert".into()),
419
+ ("children", inlines(children)),
420
+ ("attrs", attrs(a)),
421
+ ]),
422
+ InlineNode::CriticDelete(CriticDelete { attrs: a, children }) => obj(vec![
423
+ ("type", "critic_delete".into()),
424
+ ("children", inlines(children)),
425
+ ("attrs", attrs(a)),
426
+ ]),
427
+ InlineNode::CriticSubstitute(CriticSubstitute { old_text, new_text }) => obj(vec![
428
+ ("type", "critic_substitute".into()),
429
+ ("old_text", Value::String(old_text.clone())),
430
+ ("new_text", Value::String(new_text.clone())),
431
+ ]),
432
+ InlineNode::CriticComment(CriticComment { text }) => obj(vec![
433
+ ("type", "critic_comment".into()),
434
+ ("text", Value::String(text.clone())),
435
+ ]),
436
+ }
437
+ }
438
+
439
+ fn link(l: &Link) -> Value {
440
+ obj(vec![
441
+ ("type", "link".into()),
442
+ ("href", Value::String(l.href.clone())),
443
+ ("title", opt_str(&l.title)),
444
+ ("children", inlines(&l.children)),
445
+ ("ref_label", opt_str(&l.ref_label)),
446
+ ("raw_ref", opt_str(&l.raw_ref)),
447
+ ("from_crossref", Value::Bool(l.from_crossref)),
448
+ ("attrs", attrs(&l.attrs)),
449
+ ])
450
+ }
451
+
452
+ fn image(img: &Image) -> Value {
453
+ obj(vec![
454
+ ("type", "image".into()),
455
+ ("src", Value::String(img.src.clone())),
456
+ ("alt", Value::String(img.alt.clone())),
457
+ ("title", opt_str(&img.title)),
458
+ ("attrs", attrs(&img.attrs)),
459
+ ])
460
+ }
461
+
462
+ fn citation_group(g: &CitationGroup) -> Value {
463
+ obj(vec![
464
+ ("type", "citation_group".into()),
465
+ ("raw", Value::String(g.raw.clone())),
466
+ ("integral", Value::Bool(g.integral)),
467
+ (
468
+ "mode",
469
+ match g.mode {
470
+ Some(CitationRenderMode::Numbered) => "numbered".into(),
471
+ Some(CitationRenderMode::AuthorDate) => "author-date".into(),
472
+ None => Value::Null,
473
+ },
474
+ ),
475
+ (
476
+ "items",
477
+ Value::Array(g.items.iter().map(citation).collect()),
478
+ ),
479
+ ])
480
+ }
481
+
482
+ fn citation(c: &Citation) -> Value {
483
+ obj(vec![
484
+ ("key", Value::String(c.key.clone())),
485
+ ("prefix", opt_inlines(&c.prefix)),
486
+ ("locator", opt_inlines(&c.locator)),
487
+ ("locator_label", opt_str(&c.locator_label)),
488
+ ("locator_value", opt_str(&c.locator_value)),
489
+ ("suffix", opt_inlines(&c.suffix)),
490
+ ("suppress_author", Value::Bool(c.suppress_author)),
491
+ ("number", opt_usize(&c.number)),
492
+ ("label", opt_str(&c.label)),
493
+ ("use_index", opt_usize(&c.use_index)),
494
+ ])
495
+ }
496
+
497
+ fn emphasis_kind_str(k: EmphasisKind) -> &'static str {
498
+ match k {
499
+ EmphasisKind::Italic => "italic",
500
+ EmphasisKind::Strong => "strong",
501
+ EmphasisKind::Underline => "underline",
502
+ EmphasisKind::Strike => "strike",
503
+ EmphasisKind::Super => "super",
504
+ EmphasisKind::Sub => "sub",
505
+ EmphasisKind::Highlight => "highlight",
506
+ EmphasisKind::BoldItalic => "bold-italic",
507
+ }
508
+ }