1#![doc = include_str!("../README.md")]
2#![cfg_attr(not(feature = "std"), no_std)]
3
4extern crate alloc;
5
6use alloc::boxed::Box;
7use core::any::TypeId;
8use core::fmt;
9use core::fmt::Write;
10use rushdown::as_extension_data;
11use rushdown::as_extension_data_mut;
12use rushdown::as_type_data;
13use rushdown::as_type_data_mut;
14use rushdown::ast;
15use rushdown::ast::pp_indent;
16use rushdown::ast::Arena;
17use rushdown::ast::KindData;
18use rushdown::ast::NodeKind;
19use rushdown::ast::NodeRef;
20use rushdown::ast::NodeType;
21use rushdown::ast::PrettyPrint;
22use rushdown::ast::WalkStatus;
23use rushdown::matches_extension_kind;
24use rushdown::matches_kind;
25use rushdown::parser;
26use rushdown::parser::parser_extension;
27use rushdown::parser::AnyBlockParser;
28use rushdown::parser::BlockParser;
29use rushdown::parser::NoParserOptions;
30use rushdown::parser::ParserExtension;
31use rushdown::renderer;
32use rushdown::renderer::html;
33use rushdown::renderer::html::renderer_extension;
34use rushdown::renderer::html::ParagraphRendererOptions;
35use rushdown::renderer::html::RendererExtension;
36use rushdown::renderer::BoxRenderNode;
37use rushdown::renderer::NoRendererOptions;
38use rushdown::renderer::NodeRenderer;
39use rushdown::renderer::NodeRendererRegistry;
40use rushdown::renderer::RenderNode;
41use rushdown::renderer::TextWrite;
42use rushdown::text;
43use rushdown::text::Reader;
44use rushdown::util::indent_position;
45use rushdown::util::indent_width;
46use rushdown::util::is_blank;
47use rushdown::Result;
48
49#[derive(Debug)]
53pub struct DefinitionList {
54 offset: u8,
55 temp_paragraph: Option<NodeRef>,
56 is_tight: bool,
57}
58
59impl Default for DefinitionList {
60 fn default() -> Self {
61 Self::new()
62 }
63}
64
65impl DefinitionList {
66 pub fn new() -> Self {
68 Self {
69 offset: 0,
70 temp_paragraph: None,
71 is_tight: true,
72 }
73 }
74
75 fn with_offset_and_paragraph(offset: u8, temp_paragraph: NodeRef) -> Self {
76 Self {
77 offset,
78 temp_paragraph: Some(temp_paragraph),
79 is_tight: true,
80 }
81 }
82
83 #[inline(always)]
85 pub fn set_tight(&mut self, tight: bool) {
86 self.is_tight = tight;
87 }
88
89 #[inline(always)]
91 pub fn is_tight(&self) -> bool {
92 self.is_tight
93 }
94}
95
96impl NodeKind for DefinitionList {
97 fn typ(&self) -> NodeType {
98 NodeType::ContainerBlock
99 }
100
101 fn kind_name(&self) -> &'static str {
102 "DefinitionList"
103 }
104}
105
106impl PrettyPrint for DefinitionList {
107 fn pretty_print(&self, w: &mut dyn Write, _source: &str, level: usize) -> fmt::Result {
108 writeln!(w, "{}IsTight: {}", pp_indent(level), self.is_tight)?;
109 Ok(())
110 }
111}
112
113impl From<DefinitionList> for KindData {
114 fn from(e: DefinitionList) -> Self {
115 KindData::Extension(Box::new(e))
116 }
117}
118
119#[derive(Debug)]
121pub struct Term {}
122
123impl Default for Term {
124 fn default() -> Self {
125 Self::new()
126 }
127}
128
129impl Term {
130 pub fn new() -> Self {
132 Self {}
133 }
134}
135
136impl NodeKind for Term {
137 fn typ(&self) -> NodeType {
138 NodeType::LeafBlock
139 }
140
141 fn kind_name(&self) -> &'static str {
142 "Term"
143 }
144}
145
146impl PrettyPrint for Term {
147 fn pretty_print(&self, _w: &mut dyn Write, _source: &str, _level: usize) -> fmt::Result {
148 Ok(())
149 }
150}
151
152impl From<Term> for KindData {
153 fn from(e: Term) -> Self {
154 KindData::Extension(Box::new(e))
155 }
156}
157
158#[derive(Debug)]
160pub struct TermDefinition {}
161
162impl Default for TermDefinition {
163 fn default() -> Self {
164 Self::new()
165 }
166}
167
168impl TermDefinition {
169 pub fn new() -> Self {
171 Self {}
172 }
173}
174
175impl NodeKind for TermDefinition {
176 fn typ(&self) -> NodeType {
177 NodeType::ContainerBlock
178 }
179
180 fn kind_name(&self) -> &'static str {
181 "TermDefinition"
182 }
183}
184
185impl PrettyPrint for TermDefinition {
186 fn pretty_print(&self, _w: &mut dyn Write, _source: &str, _level: usize) -> fmt::Result {
187 Ok(())
188 }
189}
190
191impl From<TermDefinition> for KindData {
192 fn from(e: TermDefinition) -> Self {
193 KindData::Extension(Box::new(e))
194 }
195}
196
197#[derive(Debug, Default)]
202struct DefinitionListParser {}
203
204impl DefinitionListParser {
205 fn new() -> Self {
206 Self {}
207 }
208}
209
210impl BlockParser for DefinitionListParser {
211 fn trigger(&self) -> &[u8] {
212 b":"
213 }
214
215 fn open(
216 &self,
217 arena: &mut Arena,
218 parent_ref: NodeRef,
219 reader: &mut text::BasicReader,
220 ctx: &mut parser::Context,
221 ) -> Option<(NodeRef, parser::State)> {
222 if matches_extension_kind!(arena, parent_ref, DefinitionList) {
223 return None;
224 }
225 let (line, _) = reader.peek_line_bytes()?;
226 let pos = ctx.block_offset()?;
227 let indent = ctx.block_indent().unwrap_or(1);
228 if line[pos] != b':' || indent != 0 {
229 return None;
230 }
231 let last_ref = arena[parent_ref].last_child()?;
232 let (mut w, _) = indent_width(&line[pos + 1..], pos + 1);
233 if w < 1 {
235 return None;
236 }
237 if w > 8 {
238 w = 5;
240 }
241 w += pos + 1; if matches_kind!(arena, last_ref, Paragraph) {
244 match arena[last_ref].previous_sibling() {
245 Some(prev_ref) if matches_extension_kind!(arena, prev_ref, DefinitionList) => {
246 let kd = as_extension_data_mut!(arena, prev_ref, DefinitionList);
248 kd.offset = w as u8;
249 kd.temp_paragraph = Some(last_ref);
250 prev_ref.remove(arena);
251 Some((prev_ref, parser::State::HAS_CHILDREN))
252 }
253 _ => {
254 let list = DefinitionList::with_offset_and_paragraph(w as u8, last_ref);
256 Some((
257 arena.new_node(list),
258 parser::State::HAS_CHILDREN | parser::State::REQUIRE_PARAGRAPH,
259 ))
260 }
261 }
262 } else if matches_extension_kind!(arena, last_ref, DefinitionList) {
263 let kd = as_extension_data_mut!(arena, last_ref, DefinitionList);
265 kd.offset = w as u8;
266 kd.temp_paragraph = None;
267 last_ref.remove(arena);
268 Some((last_ref, parser::State::HAS_CHILDREN))
269 } else {
270 None
271 }
272 }
273
274 fn cont(
275 &self,
276 arena: &mut Arena,
277 node_ref: NodeRef,
278 reader: &mut text::BasicReader,
279 _ctx: &mut parser::Context,
280 ) -> Option<parser::State> {
281 let (line, _) = reader.peek_line_bytes()?;
282 if is_blank(&line) {
283 return Some(parser::State::HAS_CHILDREN);
284 }
285 let kd = as_extension_data!(arena, node_ref, DefinitionList);
286 let w = indent_width(&line, reader.line_offset()).0;
287 if w < kd.offset as usize {
288 None
289 } else {
290 let (pos, padding) = indent_position(&line, reader.line_offset(), kd.offset as usize)?;
291 reader.advance_and_set_padding(pos, padding);
292 Some(parser::State::HAS_CHILDREN)
293 }
294 }
295
296 fn can_interrupt_paragraph(&self) -> bool {
297 true
298 }
299}
300
301impl From<DefinitionListParser> for AnyBlockParser {
302 fn from(p: DefinitionListParser) -> Self {
303 AnyBlockParser::Extension(Box::new(p))
304 }
305}
306
307#[derive(Debug, Default)]
308struct TermDefinitionParser {}
309
310impl TermDefinitionParser {
311 fn new() -> Self {
312 Self {}
313 }
314}
315
316impl BlockParser for TermDefinitionParser {
317 fn trigger(&self) -> &[u8] {
318 b":"
319 }
320
321 fn open(
322 &self,
323 arena: &mut Arena,
324 parent_ref: NodeRef,
325 reader: &mut text::BasicReader,
326 ctx: &mut parser::Context,
327 ) -> Option<(NodeRef, parser::State)> {
328 let (line, _) = reader.peek_line_bytes()?;
329 let pos = ctx.block_offset()?;
330 let indent = ctx.block_indent().unwrap_or(1);
331 if line[pos] != b':' || indent != 0 {
332 return None;
333 }
334 if !matches_extension_kind!(arena, parent_ref, DefinitionList) {
335 return None;
336 }
337 let para_opt = {
338 let list_kd = as_extension_data_mut!(arena, parent_ref, DefinitionList);
339 let para_opt = list_kd.temp_paragraph;
340 list_kd.temp_paragraph = None;
341 para_opt
342 };
343 if let Some(para_ref) = para_opt {
344 let para_td = as_type_data_mut!(arena, para_ref, Block);
345 let lines = para_td.take_source();
346 for line in &lines {
347 let term_ref = arena.new_node(Term::new());
348 as_type_data_mut!(arena, term_ref, Block)
349 .append_source_line(line.trim_right_space(reader.source()));
350 parent_ref.append_child(arena, term_ref);
351 }
352 para_ref.remove(arena);
353 }
354 let list_kd = as_extension_data_mut!(arena, parent_ref, DefinitionList);
355 let (pos, padding) = indent_position(
356 &line[pos + 1..],
357 pos + 1,
358 (list_kd.offset as usize) - pos - 1,
359 )?;
360 reader.advance_and_set_padding(pos + 1, padding);
361 Some((
362 arena.new_node(TermDefinition::new()),
363 parser::State::HAS_CHILDREN,
364 ))
365 }
366
367 fn cont(
368 &self,
369 _arena: &mut Arena,
370 _node_ref: NodeRef,
371 _reader: &mut text::BasicReader,
372 _ctx: &mut parser::Context,
373 ) -> Option<parser::State> {
374 Some(parser::State::HAS_CHILDREN)
377 }
378
379 fn close(
380 &self,
381 arena: &mut Arena,
382 node_ref: NodeRef,
383 _reader: &mut text::BasicReader,
384 _ctx: &mut parser::Context,
385 ) {
386 if as_type_data!(arena, node_ref, Block).has_blank_previous_line() {
387 let mut cur = node_ref;
388 while let Some(parent_ref) = arena[cur].parent() {
389 if matches_extension_kind!(arena, parent_ref, DefinitionList) {
390 let kd = as_extension_data_mut!(arena, parent_ref, DefinitionList);
391 kd.set_tight(false);
392 break;
393 }
394 cur = parent_ref;
395 }
396 }
397 }
398
399 fn can_interrupt_paragraph(&self) -> bool {
400 true
401 }
402}
403
404impl From<TermDefinitionParser> for AnyBlockParser {
405 fn from(p: TermDefinitionParser) -> Self {
406 AnyBlockParser::Extension(Box::new(p))
407 }
408}
409
410struct DefinitionListHtmlRenderer<W: TextWrite> {
415 _phantom: core::marker::PhantomData<W>,
416 writer: html::Writer,
417}
418
419impl<W: TextWrite> DefinitionListHtmlRenderer<W> {
420 fn new(html_opts: html::Options) -> Self {
421 Self {
422 _phantom: core::marker::PhantomData,
423 writer: html::Writer::with_options(html_opts),
424 }
425 }
426}
427
428impl<W: TextWrite> RenderNode<W> for DefinitionListHtmlRenderer<W> {
429 fn render_node<'a>(
430 &self,
431 w: &mut W,
432 _source: &'a str,
433 _arena: &'a Arena,
434 _node_ref: NodeRef,
435 entering: bool,
436 _context: &mut renderer::Context,
437 ) -> Result<WalkStatus> {
438 if entering {
439 self.writer.write_safe_str(w, "<dl>\n")?
440 } else {
441 self.writer.write_safe_str(w, "</dl>\n")?
442 }
443 Ok(WalkStatus::Continue)
444 }
445}
446
447impl<'cb, W> NodeRenderer<'cb, W> for DefinitionListHtmlRenderer<W>
448where
449 W: TextWrite + 'cb,
450{
451 fn register_node_renderer_fn(self, nrr: &mut impl NodeRendererRegistry<'cb, W>) {
452 nrr.register_node_renderer_fn(TypeId::of::<DefinitionList>(), BoxRenderNode::new(self));
453 }
454}
455
456struct TermHtmlRenderer<W: TextWrite> {
457 _phantom: core::marker::PhantomData<W>,
458 writer: html::Writer,
459}
460
461impl<W: TextWrite> TermHtmlRenderer<W> {
462 fn new(html_opts: html::Options) -> Self {
463 Self {
464 _phantom: core::marker::PhantomData,
465 writer: html::Writer::with_options(html_opts),
466 }
467 }
468}
469
470impl<W: TextWrite> RenderNode<W> for TermHtmlRenderer<W> {
471 fn render_node<'a>(
472 &self,
473 w: &mut W,
474 _source: &'a str,
475 _arena: &'a Arena,
476 _node_ref: NodeRef,
477 entering: bool,
478 _context: &mut renderer::Context,
479 ) -> Result<WalkStatus> {
480 if entering {
481 self.writer.write_safe_str(w, "<dt>")?
482 } else {
483 self.writer.write_safe_str(w, "</dt>\n")?
484 }
485 Ok(WalkStatus::Continue)
486 }
487}
488
489impl<'cb, W> NodeRenderer<'cb, W> for TermHtmlRenderer<W>
490where
491 W: TextWrite + 'cb,
492{
493 fn register_node_renderer_fn(self, nrr: &mut impl NodeRendererRegistry<'cb, W>) {
494 nrr.register_node_renderer_fn(TypeId::of::<Term>(), BoxRenderNode::new(self));
495 }
496}
497
498struct TermDefinitionHtmlRenderer<W: TextWrite> {
499 _phantom: core::marker::PhantomData<W>,
500 writer: html::Writer,
501}
502
503impl<W: TextWrite> TermDefinitionHtmlRenderer<W> {
504 fn new(html_opts: html::Options) -> Self {
505 Self {
506 _phantom: core::marker::PhantomData,
507 writer: html::Writer::with_options(html_opts),
508 }
509 }
510}
511
512impl<W: TextWrite> RenderNode<W> for TermDefinitionHtmlRenderer<W> {
513 fn render_node<'a>(
514 &self,
515 w: &mut W,
516 _source: &'a str,
517 arena: &'a Arena,
518 node_ref: NodeRef,
519 entering: bool,
520 _context: &mut renderer::Context,
521 ) -> Result<WalkStatus> {
522 if entering {
523 self.writer.write_safe_str(w, "<dd>")?;
524 if let Some(p) = arena[node_ref].parent() {
525 if matches_extension_kind!(arena, p, DefinitionList) {
526 let kd = as_extension_data!(arena, p, DefinitionList);
527 if !kd.is_tight() {
528 self.writer.write_safe_str(w, "\n")?;
529 }
530 }
531 }
532 } else {
533 self.writer.write_safe_str(w, "</dd>\n")?
534 }
535 Ok(WalkStatus::Continue)
536 }
537}
538
539impl<'cb, W> NodeRenderer<'cb, W> for TermDefinitionHtmlRenderer<W>
540where
541 W: TextWrite + 'cb,
542{
543 fn register_node_renderer_fn(self, nrr: &mut impl NodeRendererRegistry<'cb, W>) {
544 nrr.register_node_renderer_fn(TypeId::of::<TermDefinition>(), BoxRenderNode::new(self));
545 }
546}
547
548#[inline(always)]
550pub fn is_in_tight_list(arena: &ast::Arena, node_ref: ast::NodeRef) -> bool {
551 if let Some(p) = arena[node_ref].parent() {
552 if let Some(gp) = arena[p].parent() {
553 if matches_extension_kind!(arena, gp, DefinitionList) {
554 let kd = as_extension_data!(arena, gp, DefinitionList);
555 return kd.is_tight();
556 }
557 }
558 }
559 html::is_in_tight_list(arena, node_ref)
560}
561
562pub fn definition_list_parser_extension() -> impl ParserExtension {
568 parser_extension(|p| {
569 p.add_block_parser(DefinitionListParser::new, NoParserOptions, 101);
570 p.add_block_parser(TermDefinitionParser::new, NoParserOptions, 102);
571 })
572}
573
574pub fn definition_list_html_renderer_extension<'cb, W>() -> impl RendererExtension<'cb, W>
576where
577 W: TextWrite + 'cb,
578{
579 renderer_extension(move |r| {
580 r.add_node_renderer(DefinitionListHtmlRenderer::new, NoRendererOptions);
581 r.add_node_renderer(TermDefinitionHtmlRenderer::new, NoRendererOptions);
582 r.add_node_renderer(TermHtmlRenderer::new, NoRendererOptions);
583 })
584 .and(html::paragraph_renderer(ParagraphRendererOptions {
585 is_in_tight_block: Some(is_in_tight_list),
586 ..Default::default()
587 }))
588}
589
590