kythe.io@v0.0.68-0.20240422202219-7225dbc01741/kythe/go/util/markedsource/markedsource.go (about) 1 /* 2 * Copyright 2016 The Kythe Authors. All rights reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 // Package markedsource defines functions for rendering MarkedSource. 18 package markedsource // import "kythe.io/kythe/go/util/markedsource" 19 20 import ( 21 "fmt" 22 "html" 23 "regexp" 24 "strings" 25 26 "kythe.io/kythe/go/util/md" 27 28 "github.com/JohannesKaufmann/html-to-markdown/escape" 29 30 cpb "kythe.io/kythe/proto/common_go_proto" 31 ) 32 33 // maxRenderDepth cuts the render algorithm if it recurses too deeply into a 34 // nested MarkedSource. The resulting identifiers will thus be partial. This 35 // value matches the kMaxRenderDepth in the C++ implementation. 36 const maxRenderDepth = 20 37 38 const invalidLookupMarker = "???" 39 40 // ContentType is a type of rendering output. 41 type ContentType string 42 43 // Supported list of ContentTypes 44 var ( 45 PlaintextContent ContentType = "txt" 46 MarkdownContent ContentType = "md" 47 ) 48 49 // content holds text and a possible destination to help us build markdown links. 50 type content struct { 51 text string 52 target string 53 } 54 55 // build creates markdown for c. If there is no target, the plain text is returned, if there is a 56 // target, a markdown link is returned. 57 func (c content) build(format ContentType) string { 58 if format == MarkdownContent { 59 if c.target == "" { 60 return c.text 61 } 62 return md.Link(c.text, c.target) 63 } 64 65 // Assume plain text if markdown isn't set. 66 return c.text 67 } 68 69 // escapeMD escapes the signature so that 1) generics don't get parsed as HTML by 70 // html-to-markdown in the Javadoc parser and 2) markdown characters in code (e.g. *) don't change 71 // how the text is displayed. 72 func escapeMD(s string) string { 73 return escape.MarkdownCharacters(html.EscapeString(s)) 74 } 75 76 type renderer struct { 77 buffer *content 78 prependBuffer string 79 bufferIsNonempty bool 80 bufferEndsInSpace bool 81 format ContentType 82 prependSpace bool 83 84 linkify func(string) string 85 } 86 87 // Render flattens MarkedSource to a string using reasonable defaults. 88 func Render(ms *cpb.MarkedSource) string { 89 enabled := map[cpb.MarkedSource_Kind]bool{} 90 for _, k := range cpb.MarkedSource_Kind_value { 91 enabled[cpb.MarkedSource_Kind(k)] = true 92 } 93 r := &renderer{ 94 buffer: &content{}, 95 format: PlaintextContent, 96 } 97 98 r.renderRoot(ms, enabled) 99 return r.buffer.build(r.format) 100 } 101 102 // RenderSignature renders the full signature from node. 103 // If not nil, linkify will be used to 104 // generate link URIs from semantic node tickets. It may return an empty string if there is no 105 // available URI. 106 func RenderSignature(node *cpb.MarkedSource, format ContentType, linkify func(string) string) string { 107 enabled := map[cpb.MarkedSource_Kind]bool{ 108 cpb.MarkedSource_IDENTIFIER: true, 109 cpb.MarkedSource_TYPE: true, 110 cpb.MarkedSource_PARAMETER: true, 111 cpb.MarkedSource_MODIFIER: true, 112 } 113 r := &renderer{ 114 buffer: &content{}, 115 linkify: linkify, 116 format: format, 117 } 118 119 r.renderRoot(node, enabled) 120 return r.buffer.build(r.format) 121 } 122 123 // RenderCallSiteSignature returns the text snippet for a callsite as plaintext. 124 func RenderCallSiteSignature(node *cpb.MarkedSource) string { 125 // This is similar to RenderSignature but does not render types to keep the text a little more 126 // compact. 127 enabled := map[cpb.MarkedSource_Kind]bool{ 128 cpb.MarkedSource_IDENTIFIER: true, 129 cpb.MarkedSource_PARAMETER: true, 130 } 131 r := &renderer{ 132 buffer: &content{}, 133 linkify: nil, 134 format: PlaintextContent, 135 } 136 137 r.renderRoot(node, enabled) 138 return r.buffer.build(r.format) 139 } 140 141 // RenderInitializer extracts and renders a plaintext initializer from node. If not nil, linkify 142 // will be used to generate link URIs from semantic node tickets. It may return an empty string if 143 // there is no available URI. 144 func RenderInitializer(node *cpb.MarkedSource, format ContentType, linkify func(string) string) string { 145 enabled := map[cpb.MarkedSource_Kind]bool{ 146 cpb.MarkedSource_INITIALIZER: true, 147 } 148 r := &renderer{ 149 buffer: &content{}, 150 linkify: linkify, 151 format: format, 152 } 153 154 r.renderRoot(node, enabled) 155 return r.buffer.build(r.format) 156 } 157 158 // RenderSimpleQualifiedName extracts and renders the simple qualified name from node. If 159 // includeIdentifier is true it includes the identifier on the qualified name. If not nil, linkify 160 // will be used to generate link URIs from semantic node tickets. It may return an empty string if 161 // there is no available URI. 162 func RenderSimpleQualifiedName(node *cpb.MarkedSource, includeIdentifier bool, format ContentType, linkify func(string) string) string { 163 enabled := map[cpb.MarkedSource_Kind]bool{ 164 cpb.MarkedSource_CONTEXT: true, 165 } 166 if includeIdentifier { 167 enabled[cpb.MarkedSource_IDENTIFIER] = true 168 } 169 r := &renderer{ 170 buffer: &content{}, 171 linkify: linkify, 172 format: format, 173 } 174 175 r.renderRoot(node, enabled) 176 return r.buffer.build(r.format) 177 } 178 179 // RenderSimpleIdentifier extracts and renders a the simple identifier from node. If not nil, 180 // linkify will be used to generate link URIs from semantic node tickets. It may return an empty 181 // string if there is no available URI. 182 func RenderSimpleIdentifier(node *cpb.MarkedSource, format ContentType, linkify func(string) string) string { 183 enabled := map[cpb.MarkedSource_Kind]bool{ 184 cpb.MarkedSource_IDENTIFIER: true, 185 } 186 r := &renderer{ 187 buffer: &content{}, 188 linkify: linkify, 189 format: format, 190 } 191 192 r.renderRoot(node, enabled) 193 return r.buffer.build(r.format) 194 } 195 196 // RenderSimpleParams extracts and renders the simple identifiers for parameters in node. If not 197 // nil, linkify will be used to generate link URIs from semantic node tickets. It may return an 198 // empty string if there is no available URI. 199 func RenderSimpleParams(node *cpb.MarkedSource, format ContentType, linkify func(string) string) []string { 200 return renderSimpleParams(node, format, linkify, 0) 201 } 202 203 func renderSimpleParams(node *cpb.MarkedSource, format ContentType, linkify func(string) string, level int) []string { 204 if level >= maxRenderDepth { 205 return nil 206 } 207 208 var out []string 209 switch node.GetKind() { 210 case cpb.MarkedSource_BOX: 211 for _, child := range node.GetChild() { 212 out = append(out, renderSimpleParams(child, format, linkify, level+1)...) 213 } 214 case cpb.MarkedSource_PARAMETER: 215 for _, child := range node.GetChild() { 216 enabled := map[cpb.MarkedSource_Kind]bool{ 217 cpb.MarkedSource_IDENTIFIER: true, 218 } 219 r := &renderer{ 220 buffer: &content{}, 221 linkify: linkify, 222 format: format, 223 } 224 r.renderChild(child, enabled, make(map[cpb.MarkedSource_Kind]bool), level+1) 225 out = append(out, r.buffer.build(r.format)) 226 } 227 case 228 cpb.MarkedSource_PARAMETER_LOOKUP_BY_PARAM, 229 cpb.MarkedSource_PARAMETER_LOOKUP_BY_PARAM_WITH_DEFAULTS, 230 cpb.MarkedSource_PARAMETER_LOOKUP_BY_TPARAM: 231 out = append(out, renderInvalidLookup(node, format)) 232 } 233 234 return out 235 } 236 237 func shouldRenderInvalidLookup(k cpb.MarkedSource_Kind, enabled map[cpb.MarkedSource_Kind]bool) bool { 238 switch k { 239 case 240 cpb.MarkedSource_PARAMETER_LOOKUP_BY_PARAM, 241 cpb.MarkedSource_PARAMETER_LOOKUP_BY_PARAM_WITH_DEFAULTS, 242 cpb.MarkedSource_PARAMETER_LOOKUP_BY_TPARAM, 243 cpb.MarkedSource_LOOKUP_BY_PARAM, 244 cpb.MarkedSource_LOOKUP_BY_TPARAM: 245 return enabled[cpb.MarkedSource_PARAMETER] 246 case cpb.MarkedSource_LOOKUP_BY_TYPED: 247 return enabled[cpb.MarkedSource_TYPE] 248 default: 249 return false 250 } 251 } 252 253 func renderInvalidLookup(node *cpb.MarkedSource, format ContentType) string { 254 return fmt.Sprintf("%s%s%s", nodePreText(node, format), invalidLookupMarker, nodePostText(node, format)) 255 } 256 257 func willRender(node *cpb.MarkedSource, enabled, under map[cpb.MarkedSource_Kind]bool, level int) bool { 258 kind := node.GetKind() 259 return level < maxRenderDepth && (kind == cpb.MarkedSource_BOX || kind == cpb.MarkedSource_IDENTIFIER || shouldRenderInvalidLookup(kind, enabled) || enabled[kind]) 260 } 261 262 func (r *renderer) renderRoot(node *cpb.MarkedSource, enabled map[cpb.MarkedSource_Kind]bool) { 263 r.renderChild(node, enabled, map[cpb.MarkedSource_Kind]bool{}, 0) 264 } 265 266 // renderChild renders the children of node up to maxRenderDepth. It only renders nodes that are 267 // present in enabled. 268 func (r *renderer) renderChild(node *cpb.MarkedSource, enabled, under map[cpb.MarkedSource_Kind]bool, level int) { 269 if level >= maxRenderDepth { 270 return 271 } 272 kind := node.GetKind() 273 if kind != cpb.MarkedSource_BOX || enabled[cpb.MarkedSource_BOX] { 274 if shouldRenderInvalidLookup(kind, enabled) { 275 r.add(renderInvalidLookup(node, r.format)) 276 return 277 } 278 if kind != cpb.MarkedSource_IDENTIFIER && !enabled[kind] { 279 return 280 } 281 // Make a copy of under for our recursive tree and add in our new kind. 282 newUnder := make(map[cpb.MarkedSource_Kind]bool) 283 for k, v := range under { 284 newUnder[k] = v 285 } 286 newUnder[kind] = true 287 under = newUnder 288 } 289 var savedContent *content 290 if shouldRender(enabled, under) { 291 l := r.linkForSource(node) 292 if l != "" && r.buffer != nil { 293 savedContent = r.buffer 294 r.buffer = &content{target: l} 295 } 296 r.add(nodePreText(node, r.format)) 297 } 298 lastRenderedChild := -1 299 for i, c := range node.GetChild() { 300 if willRender(c, enabled, under, level+1) { 301 lastRenderedChild = i 302 } 303 } 304 for i, c := range node.GetChild() { 305 if willRender(c, enabled, under, level+1) { 306 r.renderChild(c, enabled, under, level+1) 307 if lastRenderedChild > i { 308 r.add(nodePostChildText(node, r.format)) 309 } else if node.GetAddFinalListToken() { 310 r.addFinalListToken(nodePostChildText(node, r.format)) 311 } 312 } 313 } 314 if shouldRender(enabled, under) { 315 r.add(nodePostText(node, r.format)) 316 if savedContent != nil { 317 r.buffer = &content{ 318 text: savedContent.build(r.format) + r.buffer.build(r.format), 319 } 320 } 321 if node.GetKind() == cpb.MarkedSource_TYPE { 322 r.prependSpace = true 323 } 324 } 325 } 326 327 func nodePreText(node *cpb.MarkedSource, format ContentType) string { 328 if format == MarkdownContent { 329 return escapeMD(node.GetPreText()) 330 } 331 return node.GetPreText() 332 } 333 334 func nodePostText(node *cpb.MarkedSource, format ContentType) string { 335 if format == MarkdownContent { 336 return escapeMD(node.GetPostText()) 337 } 338 return node.GetPostText() 339 } 340 341 func nodePostChildText(node *cpb.MarkedSource, format ContentType) string { 342 if format == MarkdownContent { 343 return escapeMD(node.GetPostChildText()) 344 } 345 return node.GetPostChildText() 346 } 347 348 func shouldRender(enabled, under map[cpb.MarkedSource_Kind]bool) (v bool) { 349 for kind := range enabled { 350 if under[kind] { 351 return true 352 } 353 } 354 return false 355 } 356 357 // linkForSource uses linkify (if present) to turn the link in node into a like that can be used in 358 // the documentation text. 359 func (r renderer) linkForSource(node *cpb.MarkedSource) string { 360 if r.linkify == nil { 361 return "" 362 } 363 364 for _, link := range node.GetLink() { 365 for _, def := range link.GetDefinition() { 366 if l := r.linkify(def); l != "" { 367 return l 368 } 369 } 370 } 371 return "" 372 } 373 374 var excludedSpaceCharacters = regexp.MustCompile(`^(\\)?[),\]>\s].*`) 375 376 // add appends s to the current buffer text. 377 func (r *renderer) add(s string) { 378 if r.prependBuffer != "" && s != "" { 379 r.buffer.text += r.prependBuffer 380 r.bufferEndsInSpace = strings.HasSuffix(r.prependBuffer, " ") 381 r.prependBuffer = "" 382 r.bufferIsNonempty = true 383 } 384 if r.prependSpace && s != "" && r.bufferIsNonempty && !r.bufferEndsInSpace && !excludedSpaceCharacters.MatchString(s) { 385 r.buffer.text += " " 386 r.bufferEndsInSpace = true 387 } 388 r.buffer.text += s 389 if s != "" { 390 r.bufferEndsInSpace = strings.HasSuffix(s, " ") 391 r.bufferIsNonempty = true 392 r.prependSpace = false 393 } 394 } 395 396 func (r *renderer) addFinalListToken(s string) { 397 r.prependBuffer += s 398 } 399 400 // RenderQualifiedName renders a language-appropriate qualified name from a 401 // MarkedSource message. 402 func RenderQualifiedName(ms *cpb.MarkedSource) *cpb.SymbolInfo { 403 id := firstMatching(ms, func(ms *cpb.MarkedSource) bool { 404 return ms.Kind == cpb.MarkedSource_IDENTIFIER && ms.PreText != "" 405 }) 406 407 if id == nil { 408 return new(cpb.SymbolInfo) 409 } 410 411 symbolInfo := &cpb.SymbolInfo{BaseName: id.PreText} 412 413 ctx := firstMatching(ms, func(ms *cpb.MarkedSource) bool { 414 return ms.Kind == cpb.MarkedSource_CONTEXT 415 }) 416 417 if ctx != nil { 418 delim := ctx.PostChildText 419 if delim == "" { 420 delim = "." 421 } 422 var quals []string 423 for _, kid := range ctx.Child { 424 if kid.Kind == cpb.MarkedSource_IDENTIFIER || kid.Kind == cpb.MarkedSource_BOX { 425 if namespace := Render(kid); namespace != "" { 426 quals = append(quals, namespace) 427 } 428 } 429 } 430 if pkg := strings.Join(quals, delim); pkg != "" { 431 symbolInfo.QualifiedName = pkg + ctx.PostChildText + id.GetPreText() 432 } 433 } 434 435 return symbolInfo 436 } 437 438 // firstMatching returns the first node in a breadth-first traversal of the 439 // children of ms, or ms itself, for which f reports true, or nil. 440 func firstMatching(ms *cpb.MarkedSource, f func(*cpb.MarkedSource) bool) *cpb.MarkedSource { 441 if ms == nil { 442 return nil 443 } 444 if f(ms) { 445 return ms 446 } 447 for _, kid := range ms.Child { 448 if f(kid) { 449 return kid 450 } 451 } 452 for _, kid := range ms.Child { 453 if match := firstMatching(kid, f); match != nil { 454 return match 455 } 456 } 457 return nil 458 }