Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Description Lists Extension #86

Merged
merged 13 commits into from
Aug 20, 2018
Merged

Description Lists Extension #86

merged 13 commits into from
Aug 20, 2018

Conversation

ayosec
Copy link
Contributor

@ayosec ayosec commented Aug 19, 2018

This patch adds support for description lists, as described in #52.

Overview

A description-list is written as a group of terms, each one with a description (details). It is required to put a blank line between terms and descriptions.

For example, the following source:

Term 0

: Description 0

Term 1

: Description 1

    More info

Is rendered to:

<dl>
  <dt>
    <p>Term 0</p>
  </dt>
  <dd>
    <p>Description 0</p>
  </dd>
  <dt>
    <p>Term 1</p>
  </dt>
  <dd>
    <p>Description 1</p>
    <p>More info</p>
  </dd>
</dl>

It also supports nested nodes. The following source:

0

1

: 2

3

: 4

    1. 5

    2. 6
        > 7
        >
        > : 8
        >
        > 9
        >
        > : 10
        >
        > 11

Is rendered to:

<p>0</p>
<dl>
  <dt>
    <p>1</p>
  </dt>
  <dd>
    <p>2</p>
  </dd>
  <dt>
    <p>3</p>
  </dt>
  <dd>
    <p>4</p>
    <ol>
      <li>
        <p>5</p>
      </li>
      <li>
        <p>6</p>
        <blockquote>
          <dl>
            <dt>
              <p>7</p>
            </dt>
            <dd>
              <p>8</p>
            </dd>
            <dt>
              <p>9</p>
            </dt>
            <dd>
              <p>10</p>
            </dd>
          </dl>
          <p>11</p>
        </blockquote>
      </li>
    </ol>
  </dd>
</dl>

Implementation Details

I tried to imitate the support for regular lists in some parts of the implementation, mostly to support nested nodes.

There are four new node variants in NodeValue:

  • DescriptionList
  • DescriptionItem
  • DescriptionTerm
  • DescriptionDetails

DescriptionItem is used only for the parser. It is not emitted in the HTML.

To detect a description list:

  • The parser checks for a : symbol as the first non-space character.
  • When it is found, it checks if the previous node in the same container is a paragraph .
  • If it is paragraph:
    • Detach it
    • Check if the (new) last child is a DescriptionList.
      • If not, add a new DescriptionList node.
    • Creates a tree like DescriptionItem / DescriptionTerm / $old-paragraph, and append it to the DescriptionList node.
    • Finally, create a DescriptionDetails node under DescriptionItem, and use it as the new container.

A tricky part of the implementation is that I have to change the open field of an existing DescriptionList. This is required because every new term is created as a paragraph (before detect the : line with the description), so the previous list is closed. I don't know if this is the proper way to do it.

To visualize this:

If we have this source:

A

: B

C

: D

The behaviour of the parser is something like:

  • Found A:
    • Create a Paragraph node, and append it to the top container.
    • The document is:
      Document
          Paragraph "A"
      
  • Found ::
    • Detects that last child is a Paragraph, so it is detached.
    • There is no DescriptionList at the end of the container, so it creates a new one.
    • Append the detached paragraph to the new list.
    • The document is:
      Document
          DescriptionList
              DescriptionItem
                  DescriptionTerm
                      Paragraph "A"
                  DescriptionDetails
      
    • DescriptionDetails is new current container to add nodes.
  • Found B after ::
    • Creates a Paragraph and add it to the DescriptionDetails node.
  • Found C:
    • Closes the current DescriptionList and add the text as a new Paragraph.
    • The document is:
      Document
          DescriptionList
              DescriptionItem
                  DescriptionTerm
                      Paragraph "A"
                  DescriptionDetails
                      Paragraph "B"
          Paragraph "C"
      
  • Found ::
    • Detects that last child is a Paragraph, so it is detached.
    • After detach, found a DescriptionList as the last child.
      • Mark its nodes as open.
    • Append the DescriptionItem node to the current list.
    • The document is:
      Document
          DescriptionList
              DescriptionItem
                  DescriptionTerm
                      Paragraph "A"
                  DescriptionDetails
                      Paragraph "B"
              DescriptionItem
                  DescriptionTerm
                      Paragraph "C"
                  DescriptionDetails
      

The requirement of the blank line between terms and descriptions is to keep the implementation simpler. Many parsers supports a syntax like:

term 1
: description 1
term 2
: description 2

But it was not clear to me how to support this. I guess that this is something that can be added later.

Changes in the CLI

The CLI supports the flag description-lists to enable the extension.

S-Expressions in the examples

An example program in examples/s-expr.rs can be used to print the AST as S-expressions.

$ cargo run --example s-expr file1.md file2.md ...
$ cat file.md | cargo run --example s-expr 

For example, the following source:

Lorem ipsum dolor sit amet:

* Consectetur adipiscing elit
* Sed do **eiusmod** tempor incididunt
    * Ut labore et [dolore](magna-aliqua).
    * Ut enim ad minim veniam.

Generates this output:

$ cargo run --example s-expr path/to/file.md
(Document
    (Paragraph "Lorem ipsum dolor sit amet.")
    (List(NodeList { list_type: Bullet, marker_offset: 0, padding: 2, start: 1, delimiter: Period, bullet_char: 42, tight: true })
        (Item(NodeList { list_type: Bullet, marker_offset: 0, padding: 2, start: 1, delimiter: Period, bullet_char: 42, tight: false })
            (Paragraph "Consectetur adipiscing elit"))
        (Item(NodeList { list_type: Bullet, marker_offset: 0, padding: 2, start: 1, delimiter: Period, bullet_char: 42, tight: false })
            (Paragraph "Sed do " (Strong "eiusmod") " tempor incididunt")
            (List(NodeList { list_type: Bullet, marker_offset: 2, padding: 2, start: 1, delimiter: Period, bullet_char: 42, tight: true })
                (Item(NodeList { list_type: Bullet, marker_offset: 2, padding: 2, start: 1, delimiter: Period, bullet_char: 42, tight: false })
                    (Paragraph "Ut labore et " (Link(NodeLink { url: [109, 97, 103, 110, 97, 45, 97, 108, 105, 113, 117, 97], title: [] }) "dolore") "."))
                (Item(NodeList { list_type: Bullet, marker_offset: 2, padding: 2, start: 1, delimiter: Period, bullet_char: 42, tight: false })
                    (Paragraph "Ut enim ad minim veniam.")))))
)

I added it to the patch because it was useful to debug the AST, and it may be useful for other people. If you think that it should not be included, I will remove it.


Closes #52

@kivikakk
Copy link
Owner

I’m incredibly impressed. I’ll take time to review this tomorrow, but I just wanted to leave a comment now to say how amazing this is!

Copy link
Owner

@kivikakk kivikakk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just one comment!

@@ -64,6 +64,7 @@ fn main() {
"tasklist",
"superscript",
"footnotes",
"description-lists",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We add description-lists as a possible -e value here …

src/main.rs Outdated
clap::Arg::with_name("description-lists")
.long("description-lists")
.help("Parse description lists"),
)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

… and add --description-lists as a separate flag here ...

src/main.rs Outdated
@@ -125,6 +131,7 @@ fn main() {
ext_superscript: exts.remove("superscript"),
ext_header_ids: matches.value_of("header-ids").map(|s| s.to_string()),
ext_footnotes: matches.is_present("footnotes"),
ext_description_lists: matches.is_present("description-lists"),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

… and use the flag toggle here, not the exts set. As a result:

$ cargo run <(echo "a"; echo; echo ": b") --description-lists
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/comrak /dev/fd/63 --description-lists`
<dl><dt>
<p>a</p>
</dt>
<dd>
<p>b</p>
</dd>
</dl>
$ cargo run <(echo "a"; echo; echo ": b") -e description-lists
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/comrak /dev/fd/63 -e description-lists`
thread 'main' panicked at 'assertion failed: exts.is_empty()', src/main.rs:137:5
note: Run with `RUST_BACKTRACE=1` for a backtrace.
$

Ideally description lists should be an -e option. (And so should footnotes, frankly.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops.. I didn't notice that =/.

Fixed in d7469e7.

@kivikakk
Copy link
Owner

Thank you! ❤️

@kivikakk kivikakk merged commit 03c6ad3 into kivikakk:master Aug 20, 2018
@brson
Copy link
Collaborator

brson commented Aug 20, 2018

Awesome writeup and patch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Description List as an extension
3 participants