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

There are times during the development of a project where the tasks are all feature related, and then there are times where the tasks are all issue related. Instead of waiting for that second era to arrive, I instead elected to proactively invest extra time in trying to improve the scenario tests. While I know I will not find every issue, I want to make sure I do my best to throw as many combinations of elements against the wall, hoping none of those combinations will stick.1

From my viewpoint, I am aware that this focus on quality is taking time and effort away from adding new rules to PyMarkdown. However, in my eyes, every minute spent on making sure the project is stable is worth it. Even with the knowledge that I will not catch every issue before it happens, I am confident that by being proactive and adding groups of tests, I will put the project in a great position to be a stable platform for linting Markdown documents. Even better, by focusing on groups of combinations instead of individual tests, I believe I can discover a large group of issues at the current stage, instead of in a bug report later. At the very least, that is my hope, and my confidence is increasing with each group I add.

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 02 Oct 2020 and 04 Oct 2020.

Cleaning Up After Last Week

As the work I documented last week ran into the overtime territory, I decided at that time to disable those tests, with promises to work on them next week. As this is the next article, this is the next week. Time to get to work!

With very few exceptions, the results of each scenario test have been checked at least three times. As such, I was very confident that I needed to update the consistency checks to deal with these changes, and not the results. As an ancillary task, since the code to calculate the line/column number was just introduced, I knew that there was a decent chance that I might find an issue or two that would need to be fixed. As the consistency checks were not passing, I was hoping that it was the checks that needed updating but prepared for changes in the parser itself. Only research would tell, so it was on to more research!

Doing the Research

Digging into the code for links, it seemed obvious that the issue was with the links and their column numbers was something simple. As I scanned more code, my confidence that it was something simple just increased. All the big pieces of code looked fine, but one thing was sticking out: replacements. Back when I was adding the Markdown transformer, I made sure that both the HTML-friendly and Markdown-friendly versions of the token’s text were included in the Link Token. However, when I added the code to handle this into the consistency checks, I got confused.

Part of this is my own fault, but the reason I was confused was due to the duality that exists in the tokens and the project. In inline text situations, replacement sequences are used to show both the before and after parts of the replacement. In link situations, a similar pattern is used, including both parts of the replacement in the token. However, in the link case, there are a couple of added wrinkles.

The first wrinkle is that there are usually 2 different fields of the Link token that contain this information. Using the link_title field as an example, the processed version is stored in that field, while the raw text used to generate that field is kept in the pre_link_title field. To avoid useless duplication of fields, the pre_ version of the field is only used if they have a different value that their processed field. That is the second wrinkle. In processing that Link token information, it is common to see code likeL

    link_title = parent_cur_token.link_title
    if parent_cur_token.pre_link_title:
        link_title = parent_cur_token.pre_link_title

Basically, set the link_title variable to the token’s link_title field by default. But if the pre_link_title field is set, use that instead.

Given that there are two different access patterns, sometimes I can get confused and use a process for dealing with the other access pattern. And that is where the third wrinkle shows up: dealing with link labels. In the case of link labels, the processed version of the text exists between the start Link token and the end Link token, with the raw version being kept in the start Link token’s text_from_blocks field.

And to be honest, while it does make sense in how each of the 3 use cases are used in practice, remembering which one is in play when I am writing code on a given field can be difficult.

Fixing the Issues

Hopefully there is enough information in the previous section to allow for this revelation to be easy to see in advance: I used the wrong pattern in one place and the wrong field in another place. While the fix was just a couple of lines of code in the newly refactored __verify_next_inline_inline_image_inline function, it took some debugging to properly address the issue by using the correct patterns. With both patterns adjusted, most of the tests were passing.

It was in those remaining failures that I noticed that another problem that was hiding behind that problem: character references were not updating the column number. With all the confusion of the other problem out of the way, there were a couple of outlier tests dealing with character entities that were not adding any space for the Code Span tokens. That was fixed by adding the following code to the end of the handle_character_reference function:

        inline_response.delta_line_number = 0
        inline_response.delta_column_number = (
            inline_response.new_index - inline_request.next_index
        )

Once again, it was obvious that something needed to be done once I started looking at the function itself, but without something causing me to look there, it was overlooked. I was just grateful that the extra scenario tests pointed this out!

Rounding Out Image Support

While the support for images and their line/column numbers was added a long time ago, the proper validation support in the consistency checks was only recently added. In addition, while previous work tested the inline image support, there was very little testing of the shortcut and compressed image types, and no testing of the full image type.

Adding The Tests

The testing part of this support was easy to address. As part of finishing the work from the previous week, I copied over tests test_paragraph_extra_78 to test_paragraph_extra_89, renaming them test_paragraph_extra_90 to test_paragraph_extra_a2. Once that was done, I changed the example text from the link start sequence [ to the image start sequence ![. Temporarily disabling the consistency checks, I then ran through each of the scenario tests, adjusting the token output and the HTML output for the change from a Link token to an Image token. From the token point of view, I was primarily looking for the Image token to take the place of the start Link token. In addition, I was checking to ensure that any tokens after the start Link token but before (and including) the end Link token were removed. Finally, I adjusted the HTML for the scenario from an HTML anchor tag to an HTML image tag, along with any tag attribute changes that were required.

At this point, it should go without saying that as I was making those changes, I ran through the token output and the HTML output multiple times. I did these checks both mentally and my tried and true method of “counting out loud”, verifying that both values were the same. As I knew the next task was to properly implement the consistency checks for the other image types, I also knew that an extra count or two would take less time than trying to debug any off-by-one issues with any of the line/column numbers. It just made sense to take an extra minute per test and do that extra level of verification.

Reviewing Collapsed and Shortcut Images

Containing only a link label, the shortcut and collapsed image types had already been coded into the consistency check. From a quick visual inspection, they both made sense and looked great:

    elif previous_inline_token.label_type == "collapsed":
        image_alt_text = previous_inline_token.image_alt_text
        if previous_inline_token.text_from_blocks:
            image_alt_text = previous_inline_token.text_from_blocks

        token_prefix = 1
        newline_count = ParserHelper.count_newlines_in_text(image_alt_text)
        if newline_count:
            estimated_line_number += newline_count
            para_owner.rehydrate_index += newline_count
            estimated_column_number = 0

            split_label_text = image_alt_text.split("\n")
            image_alt_text = split_label_text[-1]
            token_prefix = 0

        estimated_column_number += 2
        estimated_column_number += 2 + token_prefix + len(image_alt_text)

Using the collapsed link type as an example, the algorithm was really easy to code up. A Markdown collapsed link type looks like this: [reference][]. To start with, the code to determine the content of the link label is used, as was mentioned in the previous sections. With that information in hand, the token_prefix was set to 1 to account for the opening [, before proceeding to deal with any newlines. Dealing with the newlines used the same pattern that I used for each part of the inline image type. However, the big change here is that the token_prefix is set to 0 if a newline character is found. The rationale behind this is that if there any newlines in the link label, the length of the opening [ is no longer useful in determining the column number, as it comes before the part that has a newline character in it.

With all that preparation in place, it is then time to compute the change in the column number. The initial estimated_column_number += 2 takes care of the [] characters at the end of the image text. The 2 from the first part of the following statement combines a +1 for the translation from an index into a position and a +1 for the ] character after the link label. The token_prefix variable is then added to account for the length of the opening [ label, as described above, followed by adding the length of the raw version of the link label text.

Why Review This?

It may seem weird that I reviewed this before going forward but let me explain. I reviewed both because I knew that I had plans to use either the shortcut code or the collapsed code as a template for the full image type. I knew that the only difference in the code between those two types was the addition of a link reference. To me, it just made sense to start with the code for either of those image types and just add the extra code to handle the link reference.

Following that plan to reuse that code, the support for the full image types was added within a couple of hours. After making sure the consistency checks were enabled, I started running the newly added scenario tests against the newly added consistency check code. It was then that I started noticing some weird failures.

Debugging The Alt Attribute

Digging into those failures, it was initially hard for me to figure out what the issue was. Everything looked fine until I started doing a character by character mental reconstruction of the tokens from the Markdown. It was then that I saw it: the alt attributes on a handful of the image tags were wrong.

The scenario test function test_paragraph_extra_73 is a great example of that. With the Markdown text:

a![Foβo](/uri "testing")a

and a simple knowledge of Markdown, the text assigned to the alt attribute of the HTML image tag was obvious to me. It should be Foβo. But when I looked at the token itself, that token was:

[image(1,2):inline:/uri:testing:Foo::::Foo:False:":: :]

That was close, but I was expecting a token that was more like:

[image(1,2):inline:/uri:testing:Foβo::::Foβo:False:":: :]

which contained both the processed version of the text Foβo and the unprocessed version of the text Foβo. This was not like some previous issues that I had already resolved where one of the other inline tokens was not being represented properly. This was a case of some very simple replacements needing to take place but being missed.

In addition, after looking at some other cases, backslash escape sequences were also causing issues, though usually hidden. Function test_paragraph_extra_74 is a good example where the output HTML was correct, but the tokenization contained an o for the processed text instead of Fo]o.

Fixing The Issue

To get around this issue, I was faced with three different choices. The first choice was to code something up specific to this issue. I immediately rejected that approach as I felt that one of the extensions that I will need to support in the future may have to introduce some base level of character manipulation like the character entities. As such, I wanted to leave my options open.

That left the other two options, or rather one option with two variations. In either case, I already had a mechanism for registering inline handling and using those handlers. As such, my second choice was to use the existing inline processor code for text blocks. The issue that I had with that approach was that I would need to pass an additional flag into that function that would limit its use to only the backslash escapes and the character entities. While that may have been possible with a smaller function, the size and complexity of the __process_inline_text_block function gave me have concerns about possible side effects.

From three choices down to one, I went with a simplified processor approach to the __process_inline_text_block function. When the handlers register themselves, I added a new flag to denote whether the handler was for one of the two simple inline sequences. If it was, I added the text sequence to the __valid_inline_simple_text_block_sequence_starts. Then, in the imaginatively named process_simple_inline function, I added the code for a very pared down version of the __process_inline_text_block function. This function was purposefully crafted to only handle those simple inline sequences and the newline character, nothing else.

It was when I went to wire the function up that I found another interesting surprise waiting for me.

Circular References Suck!

With a decent first attempt at a simplified inline processor in place, I did as I normally do and went to the place where I needed the function, the __consume_text_for_image_alt_text function in the LinkHelper module, and added the reference and the import to go along with it. After starting the test executing, there was a long pause, and I was then greeted with a simple error telling me:

E   ImportError: cannot import name 'LinkHelper' from 'pymarkdown.link_helper'
    (C:\old\enlistments\pymarkdown\pymarkdown\link_helper.py)

Having hit these a number of times, I was sure it was a circular import reference, but where was it? It was then that I thought about it. The LinkHelper module was already imported from the InlineProcessor module, as I used it to handle all the heavy lifting with the links. As Python does not have any concept of forward references, all referenced classes or functions must be loaded before a reference is made to that item. In this case, with the relationship already established, I could not add the reference in the other direction. I needed to find another solution.

I was sure that I would be able to come up with a more elegant solution at a later time, but I wanted to finish this task up, so I took the lazy approach of passing the function as an object. As it was passed as an argument to each function, there was no restriction imposed and I was able to use that method at the target __consume_text_for_image_alt_text function. I may want to look at a better way to do that in the future, but at the time, it worked.

Rounding The Corner

At that point, all the scenario tests were passing. I was sure that there were other scenarios that I could find and test, but I was confident that I had a good collection of paragraph tests added to the project. I knew in future weeks I would need to expand that collection in size and cover the SetExt Headings, but that could come later.

Duplicating Tests Without Duplicating Effort

With those scenario tests completed for links and images contained within paragraphs, it was time to extend those tests to including inline processing within SetExt Heading tokens. To that extent, I copied over test_paragraph_extra_43 to test_paragraph_extra_a2, creating tests test_setext_headings_extra_43 to test_setext_headings_extra_a2. In terms of the Markdown documents for each test, the only change I made was to add --- after the Markdown being tested, transforming it from a Paragraph token test to a SetExt Heading token test.

As I expected, there were some issues that I needed to resolve to get those tests working. As the original versions of the tests used Paragraph Tokens, and the handling of those Paragraph tokens relied on the rehydrate_index field of the Paragraph token, that was an obvious change that needed to be made. To be specific, I needed to add code to each reference of the rehydrate_index field to only reference that field if the Paragraph token was the container for the inline text. For the consistency check and the Inline Processor, this meant protecting any use of the para_owner variable, and the owning_paragraph_token variable for the Markdown transformer.

With that change made, the only other thing that needed to be dealt with is the change in how the leading whitespaces are handled within a Paragraph token and within a SetExt Heading token. To allow better processing of paragraphs, the Paragraph token collects any leading whitespace in the Paragraph token, whereas the processing of inline tokens within the SetExt Heading token stores that information with the inline tokens themselves. That handling was changed to ensure the information was properly being mined from each inline token.

And while it probably took a couple of hours to resolve, it felt like these changes only took a couple of minutes. Compared to some of the other changes I have done in the last couple of weeks, these changes were completed quickly. I am sure that there were a couple of issues in the code that I needed to fix, but as I said, the time just flew by.

What Was My Experience So Far?

In the introduction, I mentioned my increase in confidence for the project, mostly due to my switch from individual scenario tests to scenario test groups. From a work point of view, instead of many small issues, I am lucky if I can get 2 to 3 medium or large sized issues done in a week. But as I am covering many combinations in one task, I feel that the benefit easily outweighs the cost. And that benefit is what helps me drive forward.

And especially with today’s climate, I find that I sometimes need some help in maintaining focus on this project and driving it forward. At times like that, when I need some extra focus, I find it is good to think about the positive parts of the project. Instead of focusing on what is left to do, I focus on the progress I have made in getting to this point. Instead of focusing on the quantity of tasks I can get done in a week, I focus on the quality that those tasks being to the project. And most importantly, instead of focusing on what I cannot change around me, I choose to focus on this PyMarkdown project, and the benefits I can provide to Markdown authors with this project.

So, as I move forward with the project, focusing on quality, I choose these areas to focus on.

What is Next?

Staying with the theme of testing blocks, I tackle another couple of testing blocks in the next article.

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

~14 min read

Published

Markdown Linter Issues

Category

Software Quality

Tags

Stay in Touch