Skip to content

Commit 09a98ba

Browse files
committed
Harden series tags: italic markdown, empty-literal errors, cleanup
series_link now passes italic: true to MarkdownLinkFormatter so series names render as _italic_ in markdown mode. series_text switches from hand-rolled markdown links to MarkdownLinkFormatter with self-link suppression via LinkHelperUtils. Both tags now reject empty quoted literals ('', "", ' ') at parse time instead of silently rendering empty strings. Variables that resolve to empty at runtime are still handled by the render-time path. Changes: - Add italic: true to series_link markdown render path. - Refactor series_text render_markdown to use MdLink and LinkHelper. - Add validate_title/validate_series_name checks for empty quoted literals via regex matching. - Remove unused require 'cgi' and stale comments from both tags. - Add tests: link_text override in markdown, self-link with "The" prefix, bracket escaping, empty literal parse errors (6 new). - Update 4 existing tests from render-time empty checks to parse-time SyntaxError assertions.
1 parent b88a8fb commit 09a98ba

4 files changed

Lines changed: 155 additions & 38 deletions

File tree

_plugins/src/content/series/tags/series_link_tag.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
# _plugins/series_link_tag.rb
44
require 'jekyll'
55
require 'liquid'
6-
require 'cgi' # Keep for QuotedFragment, though CGI itself is now in LiquidUtils
76
require 'strscan'
87
require_relative '../series_link_util'
98
require_relative '../../../infrastructure/tag_argument_utils'
@@ -49,7 +48,7 @@ def render(context)
4948

5049
if context.registers[:render_mode] == :markdown
5150
data = Linker.find_series_link_data(series_title, context, link_text_override)
52-
MdLink.format_link(data, self_link: LinkHelper.self_link?(context, data[:url]))
51+
MdLink.format_link(data, italic: true, self_link: LinkHelper.self_link?(context, data[:url]))
5352
else
5453
Linker.render_series_link(series_title, context, link_text_override)
5554
end
@@ -95,8 +94,15 @@ def parse_link_text_argument(scanner)
9594
end
9695

9796
def validate_title
98-
return if @title_markup && !@title_markup.strip.empty?
97+
raise_empty_title if @title_markup.nil? || @title_markup.strip.empty?
9998

99+
m = @title_markup.match(/\A(['"])(.*)\1\z/m)
100+
return unless m
101+
102+
raise_empty_title if m[2].strip.empty?
103+
end
104+
105+
def raise_empty_title
100106
raise Liquid::SyntaxError,
101107
"Syntax Error in 'series_link': " \
102108
"Title value is missing or empty in '#{@raw_markup}'"

_plugins/src/content/series/tags/series_text_tag.rb

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
# _plugins/series_text_tag.rb
44
require 'jekyll'
55
require 'liquid'
6-
# CGI and strscan are still used by the tag's initialize
7-
require 'cgi'
86
require 'strscan'
97
require_relative '../series_link_util'
108
require_relative '../../../infrastructure/tag_argument_utils'
11-
require_relative '../series_text_utils' # Require the new utility
9+
require_relative '../series_text_utils'
10+
require_relative '../../markdown_output/markdown_link_formatter'
11+
require_relative '../../../infrastructure/links/link_helper_utils'
1212

1313
# Renders series text with appropriate context and linking.
1414
#
@@ -30,7 +30,9 @@ class SeriesTextTag < Liquid::Tag
3030
TagArgs = Jekyll::Infrastructure::TagArgumentUtils
3131
Linker = Jekyll::Series::SeriesLinkUtils
3232
TextUtil = Jekyll::Series::SeriesTextUtils
33-
private_constant :TagArgs, :Linker, :TextUtil
33+
MdLink = Jekyll::MarkdownOutput::MarkdownLinkFormatter
34+
LinkHelper = Jekyll::Infrastructure::Links::LinkHelperUtils
35+
private_constant :TagArgs, :Linker, :TextUtil, :MdLink, :LinkHelper
3436

3537
QuotedFragment = Liquid::QuotedFragment
3638

@@ -108,20 +110,24 @@ def validate_no_extra_arguments(scanner)
108110
end
109111

110112
def validate_series_name
111-
return if @series_name_markup && !@series_name_markup.strip.empty?
113+
raise_empty_name if @series_name_markup.nil? || @series_name_markup.strip.empty?
112114

115+
m = @series_name_markup.match(/\A(['"])(.*)\1\z/m)
116+
return unless m
117+
118+
raise_empty_name if m[2].strip.empty?
119+
end
120+
121+
def raise_empty_name
113122
raise Liquid::SyntaxError,
114123
"Syntax Error in 'series_text': " \
115124
"Series name value is missing or empty in '#{@raw_markup}'"
116125
end
117126

118127
def render_markdown(analysis, context, link)
119128
data = Linker.find_series_link_data(analysis[:name], context, nil, link: link)
120-
text = if data[:url]
121-
"[#{data[:display_text]}](#{data[:url]})"
122-
else
123-
data[:display_text]
124-
end
129+
self_link = LinkHelper.self_link?(context, data[:url])
130+
text = MdLink.format_link(data, italic: true, self_link: self_link)
125131
"#{analysis[:prefix]}#{text}#{analysis[:suffix]}".strip
126132
end
127133

_tests/src/content/series/tags/test_series_link_tag.rb

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,26 @@ def test_syntax_error_missing_series_title
4343
assert_match 'Could not find series title', err.message
4444
end
4545

46-
# Commenting out the problematic test for empty string literal, assuming similar issue
47-
# def test_syntax_error_series_title_empty_string_literal
48-
# err = assert_raises Liquid::SyntaxError do
49-
# Liquid::Template.parse("{% series_link '' %}")
50-
# end
51-
# assert_match "Title value is missing or empty", err.message
52-
# end
46+
def test_syntax_error_empty_single_quoted_title
47+
err = assert_raises Liquid::SyntaxError do
48+
Liquid::Template.parse("{% series_link '' %}")
49+
end
50+
assert_match 'Title value is missing or empty', err.message
51+
end
52+
53+
def test_syntax_error_empty_double_quoted_title
54+
err = assert_raises Liquid::SyntaxError do
55+
Liquid::Template.parse('{% series_link "" %}')
56+
end
57+
assert_match 'Title value is missing or empty', err.message
58+
end
59+
60+
def test_syntax_error_whitespace_only_quoted_title
61+
err = assert_raises Liquid::SyntaxError do
62+
Liquid::Template.parse("{% series_link ' ' %}")
63+
end
64+
assert_match 'Title value is missing or empty', err.message
65+
end
5366

5467
def test_syntax_error_unknown_argument
5568
err = assert_raises Liquid::SyntaxError do
@@ -137,7 +150,7 @@ def test_markdown_mode_renders_markdown_link
137150
)
138151
template = Liquid::Template.parse("{% series_link 'Hyperion Cantos' %}")
139152
output = template.render!(md_context)
140-
assert_equal '[Hyperion Cantos](/books/series/hyperion-cantos/)', output
153+
assert_equal '[_Hyperion Cantos_](/books/series/hyperion-cantos/)', output
141154
end
142155

143156
def test_markdown_mode_not_found_renders_plain_text
@@ -148,6 +161,51 @@ def test_markdown_mode_not_found_renders_plain_text
148161
)
149162
template = Liquid::Template.parse("{% series_link 'Unknown Series' %}")
150163
output = template.render!(md_context)
151-
assert_equal 'Unknown Series', output
164+
assert_equal '_Unknown Series_', output
165+
end
166+
167+
def test_markdown_mode_self_link_renders_italic_text
168+
series_page = create_doc(
169+
{ 'title' => 'Hyperion Cantos', 'layout' => 'series_page' },
170+
'/books/series/hyperion-cantos/',
171+
)
172+
site = create_site({}, {}, [series_page])
173+
md_context = create_context(
174+
{},
175+
{ site: site, page: series_page, render_mode: :markdown },
176+
)
177+
template = Liquid::Template.parse("{% series_link 'Hyperion Cantos' %}")
178+
output = template.render!(md_context)
179+
assert_equal '_Hyperion Cantos_', output
180+
end
181+
182+
def test_markdown_mode_with_link_text_override
183+
series_page = create_doc(
184+
{ 'title' => 'Hyperion Cantos', 'layout' => 'series_page' },
185+
'/books/series/hyperion-cantos/',
186+
)
187+
site = create_site({}, {}, [series_page])
188+
md_context = create_context(
189+
{},
190+
{ site: site, page: create_doc({}, '/test.html'), render_mode: :markdown },
191+
)
192+
template = Liquid::Template.parse("{% series_link 'Hyperion Cantos' link_text='the series' %}")
193+
output = template.render!(md_context)
194+
assert_equal '[_the series_](/books/series/hyperion-cantos/)', output
195+
end
196+
197+
def test_markdown_mode_escapes_brackets_in_title
198+
series_page = create_doc(
199+
{ 'title' => 'Foundation [Novels]', 'layout' => 'series_page' },
200+
'/books/series/foundation-novels/',
201+
)
202+
site = create_site({}, {}, [series_page])
203+
md_context = create_context(
204+
{},
205+
{ site: site, page: create_doc({}, '/test.html'), render_mode: :markdown },
206+
)
207+
template = Liquid::Template.parse("{% series_link 'Foundation [Novels]' %}")
208+
output = template.render!(md_context)
209+
assert_equal '[_Foundation \[Novels\]_](/books/series/foundation-novels/)', output
152210
end
153211
end

_tests/src/content/series/tags/test_series_text_tag.rb

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,10 @@ def test_series_with_variable
154154
end
155155

156156
def test_empty_input_string
157-
markup = '""'
158-
assert_equal '', render_tag(markup)
157+
err = assert_raises Liquid::SyntaxError do
158+
Liquid::Template.parse('{% series_text "" %}')
159+
end
160+
assert_match(/Series name value is missing or empty/, err.message)
159161
end
160162

161163
def test_empty_input_variable
@@ -171,8 +173,10 @@ def test_nil_input_variable
171173
end
172174

173175
def test_whitespace_input_string
174-
markup = '" "'
175-
assert_equal '', render_tag(markup)
176+
err = assert_raises Liquid::SyntaxError do
177+
Liquid::Template.parse('{% series_text " " %}')
178+
end
179+
assert_match(/Series name value is missing or empty/, err.message)
176180
end
177181

178182
def test_keyword_as_substring_does_not_prevent_suffix
@@ -189,6 +193,27 @@ def test_syntax_error_missing_argument
189193
assert_match(/Could not find series name/, err.message)
190194
end
191195

196+
def test_syntax_error_empty_single_quoted_name
197+
err = assert_raises Liquid::SyntaxError do
198+
Liquid::Template.parse("{% series_text '' %}")
199+
end
200+
assert_match(/Series name value is missing or empty/, err.message)
201+
end
202+
203+
def test_syntax_error_empty_double_quoted_name
204+
err = assert_raises Liquid::SyntaxError do
205+
Liquid::Template.parse('{% series_text "" %}')
206+
end
207+
assert_match(/Series name value is missing or empty/, err.message)
208+
end
209+
210+
def test_syntax_error_whitespace_only_quoted_name
211+
err = assert_raises Liquid::SyntaxError do
212+
Liquid::Template.parse("{% series_text ' ' %}")
213+
end
214+
assert_match(/Series name value is missing or empty/, err.message)
215+
end
216+
192217
def test_syntax_error_extra_argument
193218
err = assert_raises Liquid::SyntaxError do
194219
Liquid::Template.parse('{% series_text "Foundation" extra=arg %}')
@@ -209,31 +234,51 @@ def test_series_ending_with_series_word
209234
def test_markdown_mode_renders_linked_series
210235
md_context = create_context({}, { site: @site, page: @current_page, render_mode: :markdown })
211236
output = Liquid::Template.parse('{% series_text "Foundation" %}').render!(md_context)
212-
assert_equal 'the [Foundation](/series/foundation.html) series', output
237+
assert_equal 'the [_Foundation_](/series/foundation.html) series', output
213238
end
214239

215240
def test_markdown_mode_renders_unlinked_series
216241
md_context = create_context({}, { site: @site, page: @current_page, render_mode: :markdown })
217242
output = Liquid::Template.parse('{% series_text "Unknown" %}').render!(md_context)
218-
assert_equal 'the Unknown series', output
243+
assert_equal 'the _Unknown_ series', output
219244
end
220245

221246
def test_markdown_mode_series_starting_with_the
222247
md_context = create_context({}, { site: @site, page: @current_page, render_mode: :markdown })
223248
output = Liquid::Template.parse('{% series_text "The Expanse" %}').render!(md_context)
224-
assert_equal '[The Expanse](/series/expanse.html) series', output
249+
assert_equal '[_The Expanse_](/series/expanse.html) series', output
225250
end
226251

227252
def test_markdown_mode_series_containing_keyword
228253
md_context = create_context({}, { site: @site, page: @current_page, render_mode: :markdown })
229254
output = Liquid::Template.parse('{% series_text "Dune Saga" %}').render!(md_context)
230-
assert_equal 'the [Dune Saga](/series/dune.html)', output
255+
assert_equal 'the [_Dune Saga_](/series/dune.html)', output
231256
end
232257

233258
def test_markdown_mode_empty_input
234-
md_context = create_context({}, { site: @site, page: @current_page, render_mode: :markdown })
235-
output = Liquid::Template.parse('{% series_text "" %}').render!(md_context)
236-
assert_equal '', output
259+
err = assert_raises Liquid::SyntaxError do
260+
Liquid::Template.parse('{% series_text "" %}')
261+
end
262+
assert_match(/Series name value is missing or empty/, err.message)
263+
end
264+
265+
def test_markdown_mode_self_link_suppressed
266+
# When rendering a series page's own markdown, the link should be suppressed
267+
md_context = create_context(
268+
{},
269+
{ site: @site, page: @series1_page, render_mode: :markdown },
270+
)
271+
output = Liquid::Template.parse('{% series_text "Foundation" %}').render!(md_context)
272+
assert_equal 'the _Foundation_ series', output
273+
end
274+
275+
def test_markdown_mode_self_link_with_the_prefix
276+
md_context = create_context(
277+
{},
278+
{ site: @site, page: @series2_page, render_mode: :markdown },
279+
)
280+
output = Liquid::Template.parse('{% series_text "The Expanse" %}').render!(md_context)
281+
assert_equal '_The Expanse_ series', output
237282
end
238283

239284
# --- link=false ---
@@ -271,13 +316,13 @@ def test_link_true_explicit_still_links
271316
def test_link_false_markdown_mode_renders_plain_text
272317
md_context = create_context({}, { site: @site, page: @current_page, render_mode: :markdown })
273318
output = Liquid::Template.parse('{% series_text "Foundation" link=false %}').render!(md_context)
274-
assert_equal 'the Foundation series', output
319+
assert_equal 'the _Foundation_ series', output
275320
end
276321

277322
def test_link_false_markdown_mode_series_starting_with_the
278323
md_context = create_context({}, { site: @site, page: @current_page, render_mode: :markdown })
279324
output = Liquid::Template.parse('{% series_text "The Expanse" link=false %}').render!(md_context)
280-
assert_equal 'The Expanse series', output
325+
assert_equal '_The Expanse_ series', output
281326
end
282327

283328
def test_link_false_with_variable
@@ -294,9 +339,11 @@ def test_syntax_error_unknown_argument_still_raises
294339
assert_match(/Unexpected arguments/, err.message)
295340
end
296341

297-
def test_link_false_empty_string_returns_empty
298-
output = render_tag('"" link=false')
299-
assert_equal '', output
342+
def test_link_false_empty_string_raises
343+
err = assert_raises Liquid::SyntaxError do
344+
Liquid::Template.parse('{% series_text "" link=false %}')
345+
end
346+
assert_match(/Series name value is missing or empty/, err.message)
300347
end
301348

302349
def test_link_false_variable_resolves_to_unknown_series

0 commit comments

Comments
 (0)