Summary

In my last article, I started to add extra scenario tests that tested newline characters in various groups or themes. In this article, I continue working towards having a good level of group coverage for each of those groups.

Introduction

To be totally honest with any readers, the work that I did for this article was just more of the same stuff. Basically, look at the issues list, find something to work on, and work on it. However, as of late, I have been trying to find groups of issues to work on, instead of one-off items. As such, I was looking for issues that could help me resolve questions about a group of behavior for the project, not just a very specific behavior.

And to continue to be honest, this work was done during a difficult week for me. With things to do around the house, I did not seem to get any really good stretches of time to work on the project until later at night when I was tired. It was just one of those weeks.

But even though I was tired, I wanted to continue. While I would have been ecstatic to continue working with great velocity, what I needed to do was to maintain forward momentum. That meant picking items from the issues list that I could work on in phases, keeping that momentum up from phase to phase. That is why I wanted to focus on items I could deal with as a group. It was a good way to keep working, while not feeling I was forever stopping. For me, it was not about doing the work as much as maintaining the feeling that the work was progressing. That is what was important to me.

What Is the Audience for This Article?

While detailed more eloquently in this article, my goal for this technical article is to focus on the reasoning behind my solutions, rather that the solutions themselves. For a full record of the solutions presented in this article, please go to this project’s GitHub repository and consult the commits 07 Oct 2020 and 11 Oct 2020.

Starting With Some Cleanup

As usual, there were some issues left over from the previous week that I needed to take care of. While not pivotal to my efforts going forward, I spent a brief amount of time going through the Bugs - General - Uncategorized section of the issues list, making sure to remove anything that had already been done. This effort was mostly done for reasons of cleanliness and accuracy, though I will admit that getting rid of a handful of items that had been completed was good for my confidence.

In addition, during the prior week’s testing, I had added functions test_fenced_code_blocks_099k and `test_fenced_code_blocks_099l to test out different counts of multiple blank lines within a fenced code block. As those tests passed and seemed useful, I decided to keep them in the test suite, but did not have an issue to tag them with. It just made sense to add them at this point, before taking them any further.

Ensuring Consistent Paragraphs

The work in this section all came from one line:

- make sure line/column is tracking text indenting on each line

Not a fantastically descriptive line, but it was there. And it was enough for me to understand what it is that I wanted to look at.

In previous articles, I have talked about how paragraphs are the base building block of any Markdown document. Being the default block container for the default inline element, I would guess that an average of 75% of existing Markdown document blocks are Paragraph elements. I have no hard facts to back it up, but as I look at a representative sample of the articles I have worked on, a SWAG leads me to think that 75% is a fairly good estimate.

This perceived predominance of Paragraph elements in Markdown documents influenced my perception when I was designing the token system used by the PyMarkdown project. To accommodate this perception, I decided to place any newline handling elements in the Paragraph token instead of the encapsulated inline tokens. At the time, my thoughts were that the Paragraph element had rules that were different enough from the 2 header elements, the 2 container elements, and the 2 code block elements, that I needed to capture the element’s information differently. As Paragraph elements largely ignore leading space on a given line, the Paragraph token seemed to be the right place to store that information. While it has caused a couple of issues in the past, I still believe that it is still the right decision.

The Issue

While I do believe it was the right decision to make, that decision has added extra headaches to the project. The big headache is confirming that the newlines in the inline token correspond to newlines in the whitespace that is extracted and stored in the Paragraph token. That is where the rehydrate_index field comes in.

I have talked about it in passing, especially in a previous post in the section titled Verifying the Rehydration Index. In that article, I talk about how I was not sure if there were any errors because I was not sure that the field was named correctly. While I concluded that the field was named correctly, that name can still be confusing at times. The rehydrate_index field indicates the index of the next newline that will need processing. As such, once the processing of any text at the start of a line is done within the bound of a Paragraph token, that field is updated to 1. This offset also means that when all processing for the text within the Paragraph token has been completed, the index should be set to the number of newline characters plus 1.

To move towards consistency of this field’s value, I added the following code to the end of the processing of the end of a Paragraph block:

    num_newlines = ParserHelper.count_newlines_in_text(
        last_block_token.extracted_whitespace
    )
    if last_block_token.rehydrate_index > 1:
        assert (
            last_block_token.rehydrate_index == num_newlines
            or last_block_token.rehydrate_index == (num_newlines + 1)
        ), (
            "rehydrate_index ("
            + str(last_block_token.rehydrate_index)
            + ") != num_newlines("
            + str(num_newlines)
            + ")"
        )

Basically, if the algorithm is at the end of a Paragraph block, any inline elements contained within the paragraph should have moved the rehydrate_index field to its proper value. However, I needed to add an alternate conditional to handle the case where it was 1 count short. While that was concerning, it was more concerning that there were some cases where the rehydrate_index field even fell short of that adjusted mark. I felt it was imperative to get those outliers addressed first.

Addressing the Failures

In debugging, it is not often that an answer almost literally jumps out and screams “Here I am, please fix me!” In this case, when I executed the failing scenario tests and looked at their failures, it was an easy observation to make. For some reason, the Text token after a Hard-Line Break token included a newline character in its text section, but did not include a newline character in its whitespace section. As a result of that mismatch, the tests failed as the Paragraph token’s rehydrate_index field was not set to the correct value when verifying the tokens.

It took me a while of careful tracing through the logs, but I finally found that in the handling of the end of the line for a Hard-Line Break token, it was not clearing the whitespace_to_add variable in both case. This code:

    whitespace_to_add = ""

was being executed if the Hard-Line Break token was created by multiple space characters in the Markdown document. However, if the token was created by the backslash character at the end of the line, it was not. Making sure that code was in both branches solved some of those issues, but every test. There were still a handful of tests that failed.

Covering My Bases

While the tests for the SetExt Headings tokens were all passing, the discovery of the previous failures inspired me to add some similar tests for the SetExt Heading tokens. To do this, I started with the original scenario test, test_setext_headings_extra_22:

this was \\
---

From there, I added functions test_setext_headings_extra_22a to test_setext_headings_extra_22d to test the creation of a Hard-Line Break token followed with a Text token. To start, I added the first two functions that simply had both forms of creating a Hard-Line Break token with some simple text following it. In addition, to make sure that I was handling the next line’s leading whitespace properly, I added two more variations that included a single space character at the start of the following line.

While I am not sure how useful these four tests will be in the future, at the time they were important. As I had just fixed some issues with Paragraph tokens and extracted whitespace, I wanted to make sure that a similar format with SetExt Heading embedded text did not suffer from a similar problem.

Continuing Forward

With the experience from the last section in hand, I continued to look for the root cause of the remaining failed tests. As in the previous section, the tests were failing with a common theme: newlines that occurred within a Link token.

While the solution to this issue was not as easy to arrive at as the solution for the last section, it was fairly simple to see that the problem had two parts: the tokens derived from the link’s label, and the main body of the link itself. Almost as soon as I started looking at the logs, I noticed that the numbers were off, and I had to dig in a bit deeper.

Digging Deeper

The way inline links are constructed is as follows:

[link](/uri "title")

However, the token generation algorithms follow the way that the tokens are used to generate the HTML output, which for that Markdown is:

<p><a href="/uri" title="title">link</a></p>

As the link label (the text link in the above sample) may contain any form of inline text except for another link, that information cannot be contained in its processed form within the Link token. Instead, there is a start Link token, followed by the processed version of the link label, followed up by the end Link token. From PyMarkdown’s point of view, the token stream for the above Markdown document is:

        "[para(1,1):]",
        '[link(1,1):inline:/uri:title::::link:False:":: :]',
        "[text(1,2):link:]",
        "[end-link:::False]",
        "[end-para:::True]",

Unless the Link token contained a newline character somewhere within its bounds, everything was working properly, and the consistency checks were properly verifying the tokens. But when the Link tokens contained a newline character, those properly working algorithms were not working so well.

Finding A Solution

As I knew that this problem had two parts, I figured that the solution needed to have two parts are well. I believe that my realization of that conclusion is what enabled me to shortcut other solutions that did not work in favor of a split solution that did work.

The first half of the solution needed to deal with the text contained within the link label. While this text is handled in the tokens between the start end end Link tokens, from a Markdown point of view, it occurs right after the opening [ character of the link. The good news here is that adjusting the rehydrate_index field for each enclosed token was easily done by adding some simple code at the end of the processing loop in the __verify_inline function.

The second half of the solution was in the handling the Link token itself. As the inside inline tokens come first in the Markdown document, it made sense to handle the Link part of the solution when the end Link token is processed. This meant adding some extra code to the __verify_next_inline function, processing the end Link token at the top of the function if the current_line_token line/column numbers are both 0. If the current_line_token variable is a end Link token and the code is processing within a Paragraph token, I added new functionality to calculate the number of newline characters encountered within the Link token itself.

Running through this code in my head, and solving some simple issues, I executed the failing tests again, and was pleased to find that they were all passing. In that version of the code, I had four different sections, one for each type of link. However, after a quick examination of the code, I believed that the code could easily be parsed down to one block of code that just gathered the newline counts from each part. Eliminating all the other blocks except for the inline block, I was happy to find out that my guess was correct. The way I had set up the values for the other types of links allowed for a more simplified version of the code.

After a quick check of that guess locally, a full scenario test run confirmed that there were no ill effects of these changes, I cleaned the code up and checked in that work for the first part of the week.

Interlude

It was after this point in the week where it became more difficult to find numerous good blocks of time to work on the project. The first day it happened it was not so bad, but the struggle to find good time to work on the project was draining. I knew I had to keep my priorities focused on the other tasks in my life, as they had priority. But even so, it was just hard not to get a good block of work done on the project.

But I persevered.

Inlines and New Lines

The second half of the week was spent working on clearing up a related item from the issues list:

- need other multiline elements in label and verify

Piggybacking off the previous item, this seemed like a really good choice to pick off the issues list. While the previous work dealt with plain text inside of the link label, this issue took that work and improved on it. Instead of just simple text elements, this work adds testing support for Raw Html tokens and Code Span tokens that span multiple lines.

Starting with Tests

The start of this work was easy. I added scenario test functions test_paragraph_extra_a3 to test_paragraph_extra_c6 to cover all the new scenarios. Starting with simple tests, the first three tests that I specified were a normal link label [li\nnk], a code span [li`de\nfg`nk], and a raw html [li<de\nfg>nk]. Moving on to variations of those tests, having completed those tests for the inline link type, I transitioning to examples for the full link type, the collapsed link type, and the shortcut link type. Once those variations were done, I copied each of those tests, replacing the start Link element character [ with the Image element character ![.

Work to setup the tests was also easy, but pedantic. After a quick survey of the inline element types, I was shocked to find out that only the three inline tokens named above allow for newlines with their elements. It did make the creation of the tests easier though, so I was happy to find that out.

And maybe it was just my experience on the project, but the test setup went smoothly. I did the research, I figured out what I needed to test, and then cycled through the variations with speed and accuracy. Just felt good to have a solid grasp on the problems.

Starting with the Inline Processor

From a link point of view, the tests were generating their tokens successfully. The Text tokens and Code Span tokens were being handled properly, passing all their tests. However, when the Raw Html token tests were executed, the text text was being added to the token stream instead of the Raw Html token. Having fixed an issue in this area before, I had a feeling it was in the __collect_text_from_blocks function, where link tokens are translated back into their source text. After taking a quick look there, it was somewhat funny to see that I had handled the Code Span token case, but not the Raw Html token case. A quick fix, followed by another run over those tests, and the Link token tests that were failing started working again.

That left the failing tests that dealt with Image tokens. While this took a bit more work, it was roughly the same process as with the Link token. In this case, the text for the alt parameter of the image tag is created by consuming the elements generated from the Image token’s label field. This work is done by the __consume_text_for_image_alt_text function, consuming the tokens as the processing is done.

The main difference with this process from the link process is that, according to the GFM specification, this text is preserved largely without any of the special characters. As such, I had to check the proper translation of each of the inline tokens against BabelMark to make sure that I had the right translation. But other than that extra bump in the process, it went smoothly. The __consume_text_for_image_alt_text function filled out quickly with each coded translation.

With that task completed, the scenario tests were generating the HTML output that I expected. Onwards!

Rehydrating the Markdown Text

After I was sure that the tokens being generated correctly, I quickly ran each of the Markdown documents from the new tests through Babel Mark, verifying that the output HTML was correct. Encountering no problems, I moved on to the rehydration of those tokens into Markdown and was happy with the results. With only a couple of tests failing, I took a quick look at the failures and noticed the problem right away: rehydrating Link tokens and Image tokens that contained newlines was not working.

Following the log files, I was able to quickly figure out that the problem was that the backslash escape sequences and replacement markers were not being resolved from the text before doing the final rehydration of the elements. In the end, the following lines:

    text_to_modify = ParserHelper.remove_backspaces_from_text(text_to_modify)
    text_to_modify = ParserHelper.resolve_replacement_markers_from_text(
        text_to_modify
    )

were added to the __insert_leading_whitespace_at_newlines function to resolve those elements.

With that code added, every scenario test was passing to the point of being able to regenerate its original Markdown. On to the consistency checks!

Cleaning Up the Consistency Checks

It was in this area that I spent a lot of time making sure things were correct. Due to the shortened time blocks in which I could work on the project, the solutions that I initially came up with were just not solid enough solutions to use. These solutions were either too complicated or failed to meet the criteria, leading me to throw each approach out. In the end, I just reset to use a simple approach, after which things started to work out fine.

Learning from my previous work, I was pretty sure these changes were going to involve handling replacement markers and backslash escapes better. Specifically focusing on the link label and image label, I was able to quickly determine that the link labels and link text from the different link types in the __verify_next_inline_inline_image function needed to call the resolve_replacement_markers_from_text function to remove the replacement markers.

After making that change, I followed a hunch to see if the other changes I made needed to be copied in some form to the consistency checks. I was rewarded to find positive benefit to extending the code under the if "\n" in str(current_inline_token): condition of the __verify_inline function in a manner similar to the changes made to the __consume_text_for_image_alt_text function. It just made sense.

What Was My Experience So Far?

This was just a brutal week project-wise. The stop-and-go (but mostly stop) nature of this week reminded me of a time home when I worked in a cubicle farm for a year. Instead of people just looking over at you or emailing you, they would have to walk over to your cubicle and stand by your “door” to have a conversation with you. At least for me, it often seemed like I was just getting into a good work flow when someone else showed up to talk, causing another interruption.

While I definitely had my priorities straight in dealing with the issues around my house, the stop-and-go nature of this week made it hard to get into a good flow. Even thinking about how I felt that week made the task of writing this article more difficult. It was just that jarring at times.

That also made my current predicament that much more painful. I can see the issues list getting smaller and smaller, closer to a point where I know I will feel comfortable in releasing the project. And I want to get there, but I want to get there with what I consider to be solid quality. But I also want to get there soon. And I know that if I had more quality time during the week, I would have been able to resolve at least a couple more issues.

But I am still happy with the momentum on the project, and where I am with it. And one of the promises that I made to myself at the start of the project is that I must have balance between this project and other priorities in my life. And this was just a week where I had to put my money where my mouth was.

Hopefully next week will be better.1

What is Next?

Next week’s article will be more interesting, as I was able to address my time allotments on the project, submitting 9 commits during that week.


  1. It was better. These articles track roughly 2-3 weeks behind the actual work, I know for a fact it got better. 

Like this post? Share on: TwitterFacebookEmail

Comments

So what do you think? Did I miss something? Is any part unclear? Leave your comments below.


Reading Time

~15 min read

Published

Markdown Linter Issues

Category

Software Quality

Tags

Stay in Touch