Skip to content

Conversation

@gromain
Copy link

@gromain gromain commented Dec 30, 2025

Hello,

Following our conversation in #2818, I took a stab at this. Let me know what you think about the proposed changes.

I'm not sure how to properly format the text, ruff wanted to do some changes in other places than I've touched (mainly replacing ' with "). Let me know if it's ok to run it and make some changes in places I didn't touch. For now, I didn't use it for clarity of the changes proposed.

For the changelist:

  • I first moved the get_checkbox function to the class TasklistTreeprocessor, to simplify the call to the function.
  • I then included the md.htmlStash.store call inside the get_checkbox method.
  • I've expanded the tests to also cover the multiple lines tasklist test cases to all existing tasklist tests.
  • Finally, I've added the clickable_label option and set it so that when in use, the label include whatever markdown code (or html) is included in the task item.

I didn't change the documentation yet, and probably there are a couple of formatting issue, hence the draft on this pull request.

@gir-bot gir-bot added S: needs-review Needs to be reviewed and/or approved. C: source Related to source code. C: tasklist Related to the tasklist extension. C: tests Related to testing. labels Dec 30, 2025
@gromain gromain force-pushed the feature-tasklist-label branch from 36b39de to 756d2f5 Compare December 30, 2025 07:51
@gromain gromain marked this pull request as ready for review December 30, 2025 07:52
@facelessuser
Copy link
Owner

I will take a look when I get some time. I will state that I appreciate you putting it under a switch, as that at least helps its chances.

I am curious why the stash is needed. If you could elaborate further on that, it would help.

While I won't promise acceptance right now, I will try to approach this with an open mind. It may require changes if the behavior in some cases is deemed to be awkward.

I know right now I have concerns about certain content nested under list times, and I know you mentioned possibly leaving those out. Does this change leave them out? Or did we want to explore how well they work included first?

@facelessuser
Copy link
Owner

I'm not sure how you were running ruff, but please use python3 -m tox -e lint. That will create the virtual environment with the necessary dependencies and run with the proper rules. You can see in CI that there are no lint failures.

@facelessuser
Copy link
Owner

For proper HTML, it seems the label element is inline and should not contain block elements. This also greatly limits how it should be used when speaking about placing all the content of a list item under it.

@gromain
Copy link
Author

gromain commented Dec 30, 2025

Hello,

Thanks for the feedback.

So, the htmlStash is there so that I can "cut it in half" regarding the label element (the opening and the closing part). This allows me to embed whatever is needed in the middle, otherwise markdown code in the middle is not rendered properly (for example i elements).

Right now, I do not test for what's inside the m.group('line') element, I wasn't sure how to do that. Hence why I embed it inside the whole label. It seems to work well and I couldn't break it when trying.

However, regarding your last comment on the label element having to be inline and that it should not contain block element, could we define some test cases for this? I have a hard time figuring out how markdown would look for this.

- [ ] Maybe something like this?
- [ ] ```
     test code 
     on several
     lines
     ```

This renders as inline code (not multiline).

Also, it seems like the html standard for the label element is quite lax regarding what could be part of it, from
label element and phrasing content:

a, abbr, area (if it is a descendant of a map element), audio, b, bdi, bdo, br, button, canvas, cite, code, data, datalist, del, dfn, em, embed, i, iframe, img, input, ins, kbd, label, link (if it is allowed in the body), map, mark, MathML math, meta (if the itemprop attribute is present), meter, noscript, object, output, picture, progress, q, ruby, s, samp, script, select, selectedcontent (if it is a descendant of a button in a select), slot, small, span, strong, sub, sup, SVG svg, template, textarea, time, u, var, video, wbr, autonomous custom elements, text

All those could be elements embedded inside the label. img, code, a elements are there and allowed. li elements are not allowed, but that is not an issue for us, since sublists are out of the label anyway.

@gromain
Copy link
Author

gromain commented Dec 30, 2025

For the linter, I have an error when running python3 -m tox -e lint:

.pkg: _optional_hooks> python /usr/lib/python3.13/site-packages/pyproject_api/_backend.py True hatchling.build
.pkg: get_requires_for_build_sdist> python /usr/lib/python3.13/site-packages/pyproject_api/_backend.py True hatchling.build
.pkg: build_sdist> python /usr/lib/python3.13/site-packages/pyproject_api/_backend.py True hatchling.build
lint: install_package_deps> python -I -m pip install 'Markdown>=3.6' 'build>=1.3.0' 'coverage>=7.10.7' 'flake8>=7.3.0' 'mypy>=1.14.1' pymdown-lexers 'pyspelling>=2.12.1' 'pytest-cov>=7.0.0' 'pytest>=8.4.2' 'pyyaml>=3.10' 'requests>=2.32.5' 'ruff>=0.14.10' 'tox>=4.30.3' 'types-markdown>=3.7.0.20241204' 'types-pyyaml>=6.0.12.20241230' 'zensical>=0.0.2'
Collecting Markdown>=3.6
  Using cached markdown-3.10-py3-none-any.whl.metadata (5.1 kB)
Collecting build>=1.3.0
  Using cached build-1.3.0-py3-none-any.whl.metadata (5.6 kB)
Collecting coverage>=7.10.7
  Using cached coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl.metadata (8.5 kB)
Collecting flake8>=7.3.0
  Using cached flake8-7.3.0-py2.py3-none-any.whl.metadata (3.8 kB)
Collecting mypy>=1.14.1
  Using cached mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (2.2 kB)
ERROR: Could not find a version that satisfies the requirement pymdown-lexers (from versions: none)
ERROR: No matching distribution found for pymdown-lexers

lint: exit 1 (1.36 seconds) /home/rbazile/Documents/codes/git_repos/pymdown-extensions> python -I -m pip install 'Markdown>=3.6' 'build>=1.3.0' 'coverage>=7.10.7' 'flake8>=7.3.0' 'mypy>=1.14.1' pymdown-lexers 'pyspelling>=2.12.1' 'pytest-cov>=7.0.0' 'pytest>=8.4.2' 'pyyaml>=3.10' 'requests>=2.32.5' 'ruff>=0.14.10' 'tox>=4.30.3' 'types-markdown>=3.7.0.20241204' 'types-pyyaml>=6.0.12.20241230' 'zensical>=0.0.2' pid=349911
.pkg: _exit> python /usr/lib/python3.13/site-packages/pyproject_api/_backend.py True hatchling.build
  lint: FAIL code 1 (2.34 seconds)
  evaluation failed :( (2.39 seconds)

Probably a config error on my side, I'll have to investigate. EDIT: I fixed it. Not sure what happened in my env, but it runs properly now.

Usually, I use ruff directly in vscode or in my terminal (as its already installed on my system). I thought it would run properly when I saw the [tool.ruff] config in pyproject.toml but that's not the case (it's replacing single ' by double ").

@facelessuser
Copy link
Owner

I'm not sure what's going on with your setup. In the lint environment, there is no need to install pymdownx-lexers, what you see failing. That is only needed for documentation. It is also not needed when building the wheel for pymdown-extensions either. We can peek at the tox environment and see that pymdownx-lexers is not installed.

Screenshot 2025-12-30 at 2 23 10 PM

I will assume that there is something wonky with your VS code environment. If no issues are seen in CI here, then it is fine.

@gromain
Copy link
Author

gromain commented Dec 30, 2025

Another detail, regarding the htmlStash call. I moved it inside the get_checkbox method so that I can return both the stashed html and the match.group('line'). Before this change, what was returned by get_checkbox was sent through htmlStash but I had no way of using match.group('line') inside get_checkbox without it being left unprocessed (which makes sense if it was sent to htmlStash afterwards).

@gromain
Copy link
Author

gromain commented Dec 30, 2025

Yes, I finally fixed it in my env! Thanks for the info.

I had a weird issue (because i'm used to use uv and ran a uv sync once there as habit, if messed something somewhere).

@facelessuser
Copy link
Owner

This renders as inline code (not multiline).

This will give loose lists (with paragraphs):

Tight lists

- [ ] A
- [ ] B

Loose lists

- [ ] A

- [ ] B

Regarding the following:

- [ ] Maybe something like this?
- [ ] ```
     test code 
     on several
     lines
     ```

If you have SuperFences enabled, it requires something like this:

- [ ] Maybe something like this?
- [ ] 
    ```
    test code 
    on several
    lines
    ```

This is because of the gymnastics we have to perform to properly identify fenced code in all situations in the Python Markdown parser.

@facelessuser
Copy link
Owner

On a side note, it appears when lists are loose, that <label> is created under the paragraph, so that is good. It seems it will only apply to the first paragraph, which makes sense.

I also noticed when custom_checkbox is disabled, things are no longer clickable. This is likely because we only added labels for styling purposes with custom tasklists, and normal tasklists have not been updated to respect clickable_label. Or it is assumed because no labels are generated originally, that there is no need to add clickable labels there?

I'm still a bit confused about the stash. I feel like it shouldn't be needed. I'll have to investigate.

@facelessuser
Copy link
Owner

Please give me an example where htmlStash was required. I'm not convinced this is necessary, but I'm interested to see.

To be honest, I probably should have created real etree elements instead of just shoving text HTML in...that's probably the better solution. I did this so long ago (maybe 10 years ago), that's probably why I did it subpar with plain text.

@facelessuser
Copy link
Owner

Never mind. I see now, you were simply moving the stash call. Don't worry about it then. If this is accepted (or even if it isn't), I may clean that up.

@gromain
Copy link
Author

gromain commented Dec 30, 2025

For loose lists, I believe it works as expected, it's already one of the test cases.

I did not try with SuperFences, I'll run it and see how it goes. Will it be enough to add pymdownx.superfences: inside the correct test case extensions: list in tests/extensions/tasklist/tests.yml ?

@facelessuser
Copy link
Owner

Yeah. That's the old style tests, but it should work. I need to move tasklists tests over to the new approach, but I won't force you to do that.

@gromain
Copy link
Author

gromain commented Dec 30, 2025

When custom_checkbox is disabled, I just reused the same html as before. Hence why the label is not clickable. It's possible to indicate in the doc that clickable_label only work with custom labels.

However, I can add a proper label to this too and clean this a bit. This would allow to create clickable labels even when the style is not customised.

@gromain
Copy link
Author

gromain commented Dec 30, 2025

For the stash, yeah, it's a simple move. That's also why I put these in their own commits. Would be easier to cherry-pick those if needed.

@gromain
Copy link
Author

gromain commented Dec 30, 2025

What's the new approach? I can have a look and try my hand at it! I have known good tests there so I can port that if needed.

If I have to put my hands in tasklist, I might as well upgrade what needs an upgrade!

@facelessuser
Copy link
Owner

I guess thinking about the implementation more. The reason I probably used the stash was that no content was under the label. There was no concern about further post-processing, but now that you are moving content under the label, we should not be putting it in the stash as that will hide the content, and some things may be expecting some post-processing. We would likely need to attach proper etree elements.

' disabled' if not self.clickable_checkbox else '',
' checked' if state.lower() == 'x' else '') +
'<span class="task-list-indicator"></span></span>'
) + match.group('line') + self.md.htmlStash.store("</label>")
Copy link
Author

Choose a reason for hiding this comment

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

There, I break up the stash, so the matched group for the rest of the line gets properly rendered

@gromain
Copy link
Author

gromain commented Dec 30, 2025

That is why i break up the stash in two parts for the clickable label part of the code. Line 76, I put a comment there.

@gromain
Copy link
Author

gromain commented Dec 30, 2025

I agree though it's not very clean.

@gromain
Copy link
Author

gromain commented Dec 31, 2025

I've had a quick look this morning at the code generated when using SuperFences.
It looks like it indeed inserts div elements.
I believe this is one the cases where the label element should NOT encompass everything (otherwise it won't be proper html).

image

In this case, with my code, the </label> would end up after the </div>.

I'm not sure how to detect that the line group contains more than text that will be rendered in markdown in something that should not be used. Maybe I check for a return and only allow single line elements. That ought to do the trick to only include allowed elements in label.

I'll think about it over the New Year to see if I have an idea.

@facelessuser
Copy link
Owner

Yeah, fenced code is handles weird. It needs to be handled as a preprocessor, which is before there is an HTML tree. So they are found and inserted as placeholders. I believe when this is processing things, it's just seeing a placeholder.

@facelessuser
Copy link
Owner

This is one of the cumbersome things about Python Markdown, and why it may be more effort to achieve what you're looking for than it is really worth.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C: source Related to source code. C: tasklist Related to the tasklist extension. C: tests Related to testing. S: needs-review Needs to be reviewed and/or approved.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants