Skip to content

Commit 8b40c82

Browse files
authored
fix: add highlight support (#87)
1 parent b12ca72 commit 8b40c82

5 files changed

Lines changed: 160 additions & 3 deletions

File tree

docs/Markdown Support.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ Rockgarden supports the superset of CommonMark, GFM (GitHub Flavored Markdown),
6565

6666
| Feature | Syntax | Status | Notes |
6767
| ------------- | ------------------ | ------ | ------------------------- |
68-
| Highlights | `==text==` | | Future |
68+
| Highlights | `==text==` | | Renders as `<mark>` |
6969
| Comments | `%% comment %%` || Should strip during build |
7070
| HTML comments | `<!-- comment -->` || Standard markdown |
7171

@@ -134,7 +134,6 @@ This ensures:
134134

135135
## Future Syntax Additions
136136

137-
- Highlights: `==text==`
138137
- Comment stripping: `%% comment %%`
139138
- Block references: `[[page#^block]]`
140139
- Inline fields: `key:: value` (Dataview compatibility)

plans/tier-1-2-plan.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ One branch + PR per item. Plan → implement → test → PR → feedback → me
99
- [x] 1a. Heading link fragments not slugified
1010
- [x] 1b. Macros not processed in transclusions
1111
- [x] 2a. Math rendering (KaTeX)
12-
- [ ] 2b. Highlights (`==text==`)
12+
- [x] 2b. Highlights (`==text==`)
1313
- [ ] 2c. Comment stripping (`%% comment %%`)
1414
- [ ] 3. Nav reverse ordering
1515
- [ ] 4. Dev server custom 404
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"""Highlight syntax (==text==) for markdown-it-py.
2+
3+
Ported from the JS markdown-it-mark plugin. Registers an inline rule that
4+
converts ==text== into <mark>text</mark>. Uses the delimiter runner pattern
5+
(same as emphasis/strikethrough).
6+
"""
7+
8+
from markdown_it import MarkdownIt
9+
from markdown_it.rules_inline import StateInline
10+
from markdown_it.rules_inline.state_inline import Delimiter
11+
12+
13+
def highlight_plugin(md: MarkdownIt) -> None:
14+
md.inline.ruler.before("emphasis", "mark", _mark_tokenize)
15+
md.inline.ruler2.before("emphasis", "mark", _mark_post_process)
16+
md.add_render_rule("mark_open", _mark_open)
17+
md.add_render_rule("mark_close", _mark_close)
18+
19+
20+
def _mark_open(self, tokens, idx, options, env):
21+
return "<mark>"
22+
23+
24+
def _mark_close(self, tokens, idx, options, env):
25+
return "</mark>"
26+
27+
28+
def _mark_tokenize(state: StateInline, silent: bool) -> bool:
29+
pos = state.pos
30+
src = state.src
31+
32+
if src[pos] != "=":
33+
return False
34+
35+
if silent:
36+
return False
37+
38+
scanned = state.scanDelims(pos, True)
39+
length = scanned.length
40+
41+
if length < 2:
42+
return False
43+
44+
if length % 2:
45+
token = state.push("text", "", 0)
46+
token.content = "="
47+
length -= 1
48+
49+
i = 0
50+
while i < length:
51+
token = state.push("text", "", 0)
52+
token.content = "=="
53+
54+
state.delimiters.append(
55+
Delimiter(
56+
marker=ord("="),
57+
length=0,
58+
token=len(state.tokens) - 1,
59+
end=-1,
60+
open=scanned.can_open,
61+
close=scanned.can_close,
62+
)
63+
)
64+
65+
i += 2
66+
67+
state.pos += scanned.length
68+
return True
69+
70+
71+
def _mark_post_process(state: StateInline, *args) -> None:
72+
delimiters = state.delimiters
73+
lone_markers = []
74+
maximum = len(delimiters)
75+
76+
i = maximum - 1
77+
while i >= 0:
78+
start_delim = delimiters[i]
79+
80+
if start_delim.marker != ord("="):
81+
i -= 1
82+
continue
83+
84+
if start_delim.end == -1:
85+
i -= 1
86+
continue
87+
88+
end_delim = delimiters[start_delim.end]
89+
90+
token = state.tokens[start_delim.token]
91+
token.type = "mark_open"
92+
token.tag = "mark"
93+
token.nesting = 1
94+
token.markup = "=="
95+
token.content = ""
96+
97+
token = state.tokens[end_delim.token]
98+
token.type = "mark_close"
99+
token.tag = "mark"
100+
token.nesting = -1
101+
token.markup = "=="
102+
token.content = ""
103+
104+
inner = state.tokens[end_delim.token - 1]
105+
if inner.type == "text" and inner.content == "=":
106+
lone_markers.append(end_delim.token - 1)
107+
108+
i -= 1
109+
110+
for idx in sorted(lone_markers, reverse=True):
111+
del state.tokens[idx]

src/rockgarden/render/markdown.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from pygments.lexers import get_lexer_by_name
1313
from pygments.util import ClassNotFound
1414

15+
from rockgarden.obsidian.highlights import highlight_plugin
16+
1517
_md: MarkdownIt | None = None
1618
_highlight_formatter = HtmlFormatter(nowrap=False, cssclass="highlight")
1719

@@ -50,6 +52,7 @@ def get_markdown_renderer() -> MarkdownIt:
5052
- Task lists (- [ ] item) via mdit-py-plugins
5153
- Footnotes ([^1] references and [^1]: definitions) via mdit-py-plugins
5254
- Dollar math ($..$ inline, $$...$$ block) via mdit-py-plugins
55+
- Highlights (==text==) via custom plugin
5356
- Syntax highlighting via Pygments
5457
"""
5558
global _md
@@ -58,6 +61,7 @@ def get_markdown_renderer() -> MarkdownIt:
5861
footnote_plugin(_md)
5962
tasklists_plugin(_md)
6063
dollarmath_plugin(_md, allow_digits=False, allow_space=False)
64+
highlight_plugin(_md)
6165
_md.add_render_rule("fence", _fence_renderer)
6266
return _md
6367

tests/test_highlights.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""Tests for highlight (==text==) syntax."""
2+
3+
from rockgarden.render.markdown import render_markdown
4+
5+
6+
class TestHighlights:
7+
def test_basic_highlight(self):
8+
result = render_markdown("This is ==highlighted== text")
9+
assert "<mark>" in result
10+
assert "highlighted" in result
11+
assert "</mark>" in result
12+
13+
def test_highlight_with_inline_formatting(self):
14+
result = render_markdown("==**bold highlight**==")
15+
assert "<mark>" in result
16+
assert "<strong>" in result
17+
18+
def test_multiple_highlights(self):
19+
result = render_markdown("==one== and ==two==")
20+
assert result.count("<mark>") == 2
21+
assert result.count("</mark>") == 2
22+
23+
def test_no_empty_highlight(self):
24+
result = render_markdown("====")
25+
assert "<mark>" not in result
26+
27+
def test_highlight_in_code_block_unchanged(self):
28+
result = render_markdown("```\n==not highlighted==\n```")
29+
assert "<mark>" not in result
30+
31+
def test_inline_code_unchanged(self):
32+
result = render_markdown("`==not highlighted==`")
33+
assert "<mark>" not in result
34+
35+
def test_single_equals_not_highlight(self):
36+
result = render_markdown("a = b")
37+
assert "<mark>" not in result
38+
39+
def test_triple_equals_renders_with_extra_char(self):
40+
"""=== has an odd =, so it becomes = + ==highlight==."""
41+
result = render_markdown("===highlighted===")
42+
assert "<mark>" in result
43+
assert "highlighted" in result

0 commit comments

Comments
 (0)