@@ -63,6 +63,118 @@ fn walk_node_title<'a>(node: &'a AstNode<'a>) {
6363 }
6464}
6565
66+ /// Append the inline plain-text contribution of `node` to `out`.
67+ /// Counts `NodeValue::Text` and `NodeValue::Code` literals, and treats
68+ /// `NodeValue::SoftBreak` as a single space. Other inline containers
69+ /// (Emph, Strong, Link, etc.) descend into their children.
70+ fn collect_inline_plain_text < ' a > ( node : & ' a AstNode < ' a > , out : & mut String ) {
71+ match & node. data . borrow ( ) . value {
72+ NodeValue :: Text ( t) => out. push_str ( t) ,
73+ NodeValue :: Code ( c) => out. push_str ( & c. literal ) ,
74+ NodeValue :: SoftBreak => out. push ( ' ' ) ,
75+ _ => {
76+ for child in node. children ( ) {
77+ collect_inline_plain_text ( child, out) ;
78+ }
79+ }
80+ }
81+ }
82+
83+ /// Shorten the inline tree rooted at `node` so that, in document order,
84+ /// the cumulative plain-text contribution is exactly `target` bytes.
85+ /// Returns true once `*offset >= target`, signalling to callers that the
86+ /// rest of the tree should be detached.
87+ fn shorten_inline_to < ' a > (
88+ node : & ' a AstNode < ' a > ,
89+ offset : & mut usize ,
90+ target : usize ,
91+ ) -> bool {
92+ if * offset >= target {
93+ return true ;
94+ }
95+
96+ enum Kind {
97+ Text ,
98+ Code ,
99+ SoftBreak ,
100+ Container ,
101+ }
102+
103+ let kind = match & node. data . borrow ( ) . value {
104+ NodeValue :: Text ( _) => Kind :: Text ,
105+ NodeValue :: Code ( _) => Kind :: Code ,
106+ NodeValue :: SoftBreak => Kind :: SoftBreak ,
107+ _ => Kind :: Container ,
108+ } ;
109+
110+ match kind {
111+ Kind :: Text => {
112+ let mut data = node. data . borrow_mut ( ) ;
113+ if let NodeValue :: Text ( t) = & mut data. value {
114+ let len = t. len ( ) ;
115+ if * offset + len <= target {
116+ * offset += len;
117+ } else {
118+ let take = target - * offset;
119+ t. replace_range ( take.., "" ) ;
120+ * offset = target;
121+ }
122+ }
123+ }
124+ Kind :: Code => {
125+ let mut data = node. data . borrow_mut ( ) ;
126+ if let NodeValue :: Code ( c) = & mut data. value {
127+ let len = c. literal . len ( ) ;
128+ if * offset + len <= target {
129+ * offset += len;
130+ } else {
131+ let take = target - * offset;
132+ c. literal . replace_range ( take.., "" ) ;
133+ * offset = target;
134+ }
135+ }
136+ }
137+ Kind :: SoftBreak => {
138+ * offset += 1 ;
139+ }
140+ Kind :: Container => {
141+ let children: Vec < _ > = node. children ( ) . collect ( ) ;
142+ let mut done = false ;
143+ for child in children {
144+ if done {
145+ child. detach ( ) ;
146+ } else {
147+ done = shorten_inline_to ( child, offset, target) ;
148+ }
149+ }
150+ }
151+ }
152+
153+ * offset >= target
154+ }
155+
156+ /// If the first paragraph's trimmed plain text ends with `':'` and
157+ /// contains a `'.'`, shorten the paragraph so the rendered output ends
158+ /// right after the last `'.'`. Does nothing otherwise. See
159+ /// <https://github.com/denoland/deno_doc/issues/633>.
160+ fn shorten_title_summary_trailing_colon < ' a > ( paragraph : & ' a AstNode < ' a > ) {
161+ let mut plain = String :: new ( ) ;
162+ collect_inline_plain_text ( paragraph, & mut plain) ;
163+
164+ let trimmed = plain. trim_end_matches ( |c : char | c. is_ascii_whitespace ( ) ) ;
165+ if !trimmed. ends_with ( ':' ) {
166+ return ;
167+ }
168+
169+ let Some ( last_dot) = trimmed. rfind ( '.' ) else {
170+ return ;
171+ } ;
172+
173+ let target = last_dot + 1 ;
174+ let mut offset = 0 ;
175+ shorten_inline_to ( paragraph, & mut offset, target) ;
176+ }
177+
66178pub fn render_node < ' a > (
67179 node : & ' a AstNode < ' a > ,
68180 options : & comrak:: Options ,
@@ -152,6 +264,7 @@ pub fn create_renderer(
152264 walk_node_title ( root) ;
153265
154266 if let Some ( child) = root. first_child ( ) {
267+ shorten_title_summary_trailing_colon ( child) ;
155268 render_node ( child, & options, & plugins)
156269 } else {
157270 return None ;
@@ -254,3 +367,63 @@ impl SyntaxHighlighterAdapter for ComrakHighlightWrapperAdapter {
254367 }
255368 }
256369}
370+
371+ #[ cfg( test) ]
372+ mod tests {
373+ use super :: * ;
374+
375+ fn render_title_only ( md : & str ) -> String {
376+ let mut options = default_options ( ) ;
377+ options. render . escape = true ;
378+
379+ let arena = Arena :: new ( ) ;
380+ let root = comrak:: parse_document ( & arena, md, & options) ;
381+ walk_node_title ( root) ;
382+
383+ let Some ( child) = root. first_child ( ) else {
384+ return String :: new ( ) ;
385+ } ;
386+ shorten_title_summary_trailing_colon ( child) ;
387+ render_node ( child, & options, & comrak:: Plugins :: default ( ) )
388+ }
389+
390+ #[ test]
391+ fn title_summary_trailing_colon_with_period_shortens_to_last_period ( ) {
392+ let html = render_title_only ( "This is a summary. It says:" ) ;
393+ assert_eq ! ( html. trim( ) , "<p>This is a summary.</p>" ) ;
394+ }
395+
396+ #[ test]
397+ fn title_summary_trailing_colon_without_period_left_unchanged ( ) {
398+ let html = render_title_only ( "This is a summary:" ) ;
399+ assert_eq ! ( html. trim( ) , "<p>This is a summary:</p>" ) ;
400+ }
401+
402+ #[ test]
403+ fn title_summary_no_trailing_colon_left_unchanged ( ) {
404+ let html = render_title_only ( "This is a summary." ) ;
405+ assert_eq ! ( html. trim( ) , "<p>This is a summary.</p>" ) ;
406+ }
407+
408+ #[ test]
409+ fn title_summary_trailing_colon_after_code_span_shortens ( ) {
410+ // Period before the code+colon tail — backtracking should drop the
411+ // code span and trailing colon entirely.
412+ let html = render_title_only ( "First. Then `something`:" ) ;
413+ assert_eq ! ( html. trim( ) , "<p>First.</p>" ) ;
414+ }
415+
416+ #[ test]
417+ fn title_summary_trailing_colon_after_emph_shortens ( ) {
418+ let html = render_title_only ( "First. Then *emph*:" ) ;
419+ assert_eq ! ( html. trim( ) , "<p>First.</p>" ) ;
420+ }
421+
422+ #[ test]
423+ fn title_summary_period_inside_code_span_keeps_code_up_to_period ( ) {
424+ // The last '.' lives inside a code span; shortening lands exactly at
425+ // the end of that span and drops the following text+colon.
426+ let html = render_title_only ( "Some `code.` text:" ) ;
427+ assert_eq ! ( html. trim( ) , "<p>Some <code>code.</code></p>" ) ;
428+ }
429+ }
0 commit comments