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  }