Skip to content

Implement JS-esque StateBase.srcCharCodeAt - #190

Closed
hukkin wants to merge 6 commits into
executablebooks:masterfrom
hukkin:charcodeat
Closed

Implement JS-esque StateBase.srcCharCodeAt#190
hukkin wants to merge 6 commits into
executablebooks:masterfrom
hukkin:charcodeat

Conversation

@hukkin

@hukkin hukkin commented Jan 24, 2022

Copy link
Copy Markdown
Contributor

Replaces use of StateBase.srcCharCode with the more JS-esque StateBase.srcCharCodeAt that can return None.

I didn't remove StateBase.srcCharCode for ease of migration because mdit-py-plugins use it. But did deprecate it.

Happy to hear what you think @chrisjsewell . Not sure if this is the solution or migration strategy you want, but it's something 😄

I haven't measured performance yet. We may want to do that if this is something we want to proceed with otherwise

EDIT: This PR is in response to discussion in #186

@codecov

codecov Bot commented Jan 24, 2022

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.89474% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.11%. Comparing base (d6adf66) to head (6323f8b).
⚠️ Report is 104 commits behind head on master.

Files with missing lines Patch % Lines
markdown_it/ruler.py 83.33% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #190      +/-   ##
==========================================
- Coverage   96.17%   96.11%   -0.07%     
==========================================
  Files          61       61              
  Lines        3267     3267              
==========================================
- Hits         3142     3140       -2     
- Misses        125      127       +2     
Flag Coverage Δ
pytests 96.11% <97.89%> (-0.07%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@hukkin

hukkin commented Jan 25, 2022

Copy link
Copy Markdown
Contributor Author

Seeing that this PR only removes 4 instances of try: ... except IndexError: handling and that almost always we do an if pos < maximum: check or similar to ensure None is not an option, I actually still think it may be a fine decision to reject this change, and simply be more strict/explicit about out-of-range indices than upstream is. The current state is also better for performance. Arguably we'd be throwing away some work that has gone into making upstream more type safe.

The downsides for rejecting are diverging slightly from upstream logic, and the chance that there's still some place in the code where None handling is required but we don't know.

@hukkin hukkin mentioned this pull request Feb 10, 2022
chrisjsewell added a commit that referenced this pull request Sep 8, 2026
## Summary
Fixes #415. Input that ends on a blockquote marker while a table is open
inside that quote raises `IndexError: string index out of range`:

```python
from markdown_it import MarkdownIt

# html_block.py — raises IndexError
MarkdownIt().enable("table").parse("> | a | b |\n> |---|---|\n>")

# heading.py — same input, raises once html_block is out of the way
MarkdownIt("commonmark", {"html": False}).enable("table").parse("> | a | b |\n> |---|---|\n>")
```

## Root cause
With `table` enabled, `html_block` and `heading` run as **terminator
rules** on the empty final line that a trailing `>` produces. There,
`pos = state.bMarks[startLine] + state.tShift[startLine]` equals
`len(state.src)`, and both rules index `state.src[pos]` without guarding
that boundary:

- `html_block.py`: `if state.src[pos] != "<":`
- `heading.py`: `ch = state.src[pos]` — its `pos >= maximum` check is on
the *next* line, after the index

In markdown-it (JS) the equivalent `state.src.charCodeAt(pos)` returns
`NaN` out of range instead of raising — the port hazard tracked in #190.

## Fix
Sibling terminator rules already defend against exactly this — `hr.py`
and `blockquote.py` wrap the same index access in `try: ... except
IndexError: return False` (added for #185 / #204). `html_block.py` and
`heading.py` were missed. This applies the same established guard to
both.

`html_block` runs before `heading`, so it shadows it (disabling `html`
moves the traceback to `heading` rather than fixing it) — hence both
rules need the guard, and there is a regression test for each path.

## Tests
Added two regression tests in `tests/test_fuzzer.py` (the existing home
for crash-regression cases), one per crash path — asserting the input
renders without raising.

## Note on local verification
I verified the guard logic in isolation (the old expression raises
`IndexError` when `pos == len(src)`; the guarded version returns
`False`, i.e. the rule declines, with identical behavior for in-range
characters — matching how `hr.py`/`blockquote.py` already behave). I was
not able to run the full pytest suite locally (my environment's Python
predates this package's minimum), so I'd appreciate CI confirming the
two new tests pass and that the rendered output for that input is
sensible. Happy to adjust the tests to assert exact rendered HTML if
you'd prefer that over "does not raise".

Co-authored-by: saket3395 <sakettulsan95@gmail.com>
Co-authored-by: Chris Sewell <chrisj_sewell@hotmail.com>

Copy link
Copy Markdown
Member

Closing as unrebaseable: srcCharCode, which this builds on, has since been removed from master, and the branch conflicts across most of the parser. The underlying question (should out-of-range reads return None like JS charCodeAt, or keep raising?) is still worth deciding; #415/#416 were another instance of that class of bug, so let's track it as an issue rather than this branch.


Generated by Claude Code

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.

2 participants