Skip to content

Commit

Permalink
feat(internal/godocfx): add prettyprint class to code blocks (#3819)
Browse files Browse the repository at this point in the history
Fixes #3804.
  • Loading branch information
tbpg committed Mar 16, 2021
1 parent df28999 commit 6e49f21
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 60 deletions.
74 changes: 74 additions & 0 deletions internal/godocfx/goldmark-codeblock/codeblock.go
@@ -0,0 +1,74 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package goldmarkcodeblock

import (
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
)

// codeBlockHTMLRenderer is a renderer.NodeRenderer implementation that
// renders CodeBlock nodes.
type codeBlockHTMLRenderer struct {
html.Config
}

// newCodeBlockHTMLRenderer returns a new CodeblockHTMLRenderer.
func newCodeBlockHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
r := &codeBlockHTMLRenderer{
Config: html.NewConfig(),
}
for _, opt := range opts {
opt.SetHTMLOption(&r.Config)
}
return r
}

func (r *codeBlockHTMLRenderer) renderCodeBlock(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
if entering {
_, _ = w.WriteString(`<pre><code class="prettyprint">`)

l := n.Lines().Len()
for i := 0; i < l; i++ {
line := n.Lines().At(i)
r.Writer.RawWrite(w, line.Value(source))
}
} else {
_, err := w.WriteString("</code></pre>")
if err != nil {
return ast.WalkContinue, err
}
}
return ast.WalkContinue, nil
}

// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
func (r *codeBlockHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindFencedCodeBlock, r.renderCodeBlock)
}

type codeBlock struct{}

// CodeBlock is an extenstion to add class="prettyprint" to code blocks.
var CodeBlock = &codeBlock{}

func (c *codeBlock) Extend(m goldmark.Markdown) {
m.Renderer().AddOptions(renderer.WithNodeRenderers(
util.Prioritized(newCodeBlockHTMLRenderer(), 1),
))
}
3 changes: 2 additions & 1 deletion internal/godocfx/parse.go
Expand Up @@ -38,6 +38,7 @@ import (
"strconv" "strconv"
"strings" "strings"


goldmarkcodeblock "cloud.google.com/go/internal/godocfx/goldmark-codeblock"
"cloud.google.com/go/third_party/go/doc" "cloud.google.com/go/third_party/go/doc"
"cloud.google.com/go/third_party/pkgsite" "cloud.google.com/go/third_party/pkgsite"
"github.com/yuin/goldmark" "github.com/yuin/goldmark"
Expand Down Expand Up @@ -581,7 +582,7 @@ func toHTML(s string) string {
doc.ToMarkdown(buf, s, nil) doc.ToMarkdown(buf, s, nil)


// Then, handle Markdown stuff, like lists and links. // Then, handle Markdown stuff, like lists and links.
md := goldmark.New(goldmark.WithRendererOptions(html.WithUnsafe())) md := goldmark.New(goldmark.WithRendererOptions(html.WithUnsafe()), goldmark.WithExtensions(goldmarkcodeblock.CodeBlock))
mdBuf := &bytes.Buffer{} mdBuf := &bytes.Buffer{}
if err := md.Convert(buf.Bytes(), mdBuf); err != nil { if err := md.Convert(buf.Bytes(), mdBuf); err != nil {
panic(err) panic(err)
Expand Down
122 changes: 63 additions & 59 deletions internal/godocfx/testdata/golden/index.yml
Expand Up @@ -12,82 +12,86 @@ items:
as described in\nhttps://cloud.google.com/storage/docs/exponential-backoff. Retrying as described in\nhttps://cloud.google.com/storage/docs/exponential-backoff. Retrying
continues\nindefinitely unless the controlling context is canceled or the client continues\nindefinitely unless the controlling context is canceled or the client
is closed. See\ncontext.WithTimeout and context.WithCancel.</p>\n<h3>Creating is closed. See\ncontext.WithTimeout and context.WithCancel.</p>\n<h3>Creating
a Client</h3>\n<p>To start working with this package, create a client:</p>\n<pre><code>ctx a Client</h3>\n<p>To start working with this package, create a client:</p>\n<pre><code
:= context.Background()\nclient, err := storage.NewClient(ctx)\nif err != nil class=\"prettyprint\">ctx := context.Background()\nclient, err := storage.NewClient(ctx)\nif
{\n // TODO: Handle error.\n}\n</code></pre>\n<p>The client will use your default err != nil {\n // TODO: Handle error.\n}\n</code></pre><p>The client will use
application credentials. Clients should be\nreused instead of created as needed. your default application credentials. Clients should be\nreused instead of created
The methods of Client are safe for\nconcurrent use by multiple goroutines.</p>\n<p>If as needed. The methods of Client are safe for\nconcurrent use by multiple goroutines.</p>\n<p>If
you only wish to access public data, you can create\nan unauthenticated client you only wish to access public data, you can create\nan unauthenticated client
with</p>\n<pre><code>client, err := storage.NewClient(ctx, option.WithoutAuthentication())\n</code></pre>\n<h3>Buckets</h3>\n<p>A with</p>\n<pre><code class=\"prettyprint\">client, err := storage.NewClient(ctx,
Google Cloud Storage bucket is a collection of objects. To work with a\nbucket, option.WithoutAuthentication())\n</code></pre><h3>Buckets</h3>\n<p>A Google Cloud
make a bucket handle:</p>\n<pre><code>bkt := client.Bucket(bucketName)\n</code></pre>\n<p>A Storage bucket is a collection of objects. To work with a\nbucket, make a bucket
handle:</p>\n<pre><code class=\"prettyprint\">bkt := client.Bucket(bucketName)\n</code></pre><p>A
handle is a reference to a bucket. You can have a handle even if the\nbucket doesn't handle is a reference to a bucket. You can have a handle even if the\nbucket doesn't
exist yet. To create a bucket in Google Cloud Storage,\ncall Create on the handle:</p>\n<pre><code>if exist yet. To create a bucket in Google Cloud Storage,\ncall Create on the handle:</p>\n<pre><code
err := bkt.Create(ctx, projectID, nil); err != nil {\n // TODO: Handle error.\n}\n</code></pre>\n<p>Note class=\"prettyprint\">if err := bkt.Create(ctx, projectID, nil); err != nil {\n
that although buckets are associated with projects, bucket names are\nglobal across \ // TODO: Handle error.\n}\n</code></pre><p>Note that although buckets are
all projects.</p>\n<p>Each bucket has associated metadata, represented in this associated with projects, bucket names are\nglobal across all projects.</p>\n<p>Each
package by\nBucketAttrs. The third argument to BucketHandle.Create allows you bucket has associated metadata, represented in this package by\nBucketAttrs. The
to set\nthe initial BucketAttrs of a bucket. To retrieve a bucket's attributes, third argument to BucketHandle.Create allows you to set\nthe initial BucketAttrs
use\nAttrs:</p>\n<pre><code>attrs, err := bkt.Attrs(ctx)\nif err != nil {\n // of a bucket. To retrieve a bucket's attributes, use\nAttrs:</p>\n<pre><code class=\"prettyprint\">attrs,
TODO: Handle error.\n}\nfmt.Printf(&quot;bucket %s, created at %s, is located err := bkt.Attrs(ctx)\nif err != nil {\n // TODO: Handle error.\n}\nfmt.Printf(&quot;bucket
in %s with storage class %s\\n&quot;,\n attrs.Name, attrs.Created, attrs.Location, %s, created at %s, is located in %s with storage class %s\\n&quot;,\n attrs.Name,
attrs.StorageClass)\n</code></pre>\n<h3>Objects</h3>\n<p>An object holds arbitrary attrs.Created, attrs.Location, attrs.StorageClass)\n</code></pre><h3>Objects</h3>\n<p>An
data as a sequence of bytes, like a file. You\nrefer to objects using a handle, object holds arbitrary data as a sequence of bytes, like a file. You\nrefer to
just as with buckets, but unlike buckets\nyou don't explicitly create an object. objects using a handle, just as with buckets, but unlike buckets\nyou don't explicitly
Instead, the first time you write\nto an object it will be created. You can use create an object. Instead, the first time you write\nto an object it will be created.
the standard Go io.Reader\nand io.Writer interfaces to read and write object data:</p>\n<pre><code>obj You can use the standard Go io.Reader\nand io.Writer interfaces to read and write
:= bkt.Object(&quot;data&quot;)\n// Write something to obj.\n// w implements io.Writer.\nw object data:</p>\n<pre><code class=\"prettyprint\">obj := bkt.Object(&quot;data&quot;)\n//
:= obj.NewWriter(ctx)\n// Write some text to obj. This will either create the Write something to obj.\n// w implements io.Writer.\nw := obj.NewWriter(ctx)\n//
object or overwrite whatever is there already.\nif _, err := fmt.Fprintf(w, &quot;This Write some text to obj. This will either create the object or overwrite whatever
object contains text.\\n&quot;); err != nil {\n // TODO: Handle error.\n}\n// is there already.\nif _, err := fmt.Fprintf(w, &quot;This object contains text.\\n&quot;);
Close, just like writing a file.\nif err := w.Close(); err != nil {\n // TODO: err != nil {\n // TODO: Handle error.\n}\n// Close, just like writing a file.\nif
Handle error.\n}\n\n// Read it back.\nr, err := obj.NewReader(ctx)\nif err != err := w.Close(); err != nil {\n // TODO: Handle error.\n}\n\n// Read it back.\nr,
nil {\n // TODO: Handle error.\n}\ndefer r.Close()\nif _, err := io.Copy(os.Stdout, err := obj.NewReader(ctx)\nif err != nil {\n // TODO: Handle error.\n}\ndefer
r); err != nil {\n // TODO: Handle error.\n}\n// Prints &quot;This object contains r.Close()\nif _, err := io.Copy(os.Stdout, r); err != nil {\n // TODO: Handle
text.&quot;\n</code></pre>\n<p>Objects also have attributes, which you can fetch error.\n}\n// Prints &quot;This object contains text.&quot;\n</code></pre><p>Objects
with Attrs:</p>\n<pre><code>objAttrs, err := obj.Attrs(ctx)\nif err != nil {\n also have attributes, which you can fetch with Attrs:</p>\n<pre><code class=\"prettyprint\">objAttrs,
\ // TODO: Handle error.\n}\nfmt.Printf(&quot;object %s has size %d and can err := obj.Attrs(ctx)\nif err != nil {\n // TODO: Handle error.\n}\nfmt.Printf(&quot;object
be read using %s\\n&quot;,\n objAttrs.Name, objAttrs.Size, objAttrs.MediaLink)\n</code></pre>\n<h3>Listing %s has size %d and can be read using %s\\n&quot;,\n objAttrs.Name, objAttrs.Size,
objects</h3>\n<p>Listing objects in a bucket is done with the Bucket.Objects method:</p>\n<pre><code>query objAttrs.MediaLink)\n</code></pre><h3>Listing objects</h3>\n<p>Listing objects
in a bucket is done with the Bucket.Objects method:</p>\n<pre><code class=\"prettyprint\">query
:= &amp;storage.Query{Prefix: &quot;&quot;}\n\nvar names []string\nit := bkt.Objects(ctx, := &amp;storage.Query{Prefix: &quot;&quot;}\n\nvar names []string\nit := bkt.Objects(ctx,
query)\nfor {\n attrs, err := it.Next()\n if err == iterator.Done {\n break\n query)\nfor {\n attrs, err := it.Next()\n if err == iterator.Done {\n break\n
\ }\n if err != nil {\n log.Fatal(err)\n }\n names = append(names, \ }\n if err != nil {\n log.Fatal(err)\n }\n names = append(names,
attrs.Name)\n}\n</code></pre>\n<p>If only a subset of object attributes is needed attrs.Name)\n}\n</code></pre><p>If only a subset of object attributes is needed
when listing, specifying this\nsubset using Query.SetAttrSelection may speed up when listing, specifying this\nsubset using Query.SetAttrSelection may speed up
the listing process:</p>\n<pre><code>query := &amp;storage.Query{Prefix: &quot;&quot;}\nquery.SetAttrSelection([]string{&quot;Name&quot;})\n\n// the listing process:</p>\n<pre><code class=\"prettyprint\">query := &amp;storage.Query{Prefix:
... as before\n</code></pre>\n<h3>ACLs</h3>\n<p>Both objects and buckets have &quot;&quot;}\nquery.SetAttrSelection([]string{&quot;Name&quot;})\n\n// ... as
ACLs (Access Control Lists). An ACL is a list of\nACLRules, each of which specifies before\n</code></pre><h3>ACLs</h3>\n<p>Both objects and buckets have ACLs (Access
the role of a user, group or project. ACLs\nare suitable for fine-grained control, Control Lists). An ACL is a list of\nACLRules, each of which specifies the role
but you may prefer using IAM to control\naccess at the project level (see\nhttps://cloud.google.com/storage/docs/access-control/iam).</p>\n<p>To of a user, group or project. ACLs\nare suitable for fine-grained control, but
list the ACLs of a bucket or object, obtain an ACLHandle and call its List method:</p>\n<pre><code>acls, you may prefer using IAM to control\naccess at the project level (see\nhttps://cloud.google.com/storage/docs/access-control/iam).</p>\n<p>To
err := obj.ACL().List(ctx)\nif err != nil {\n // TODO: Handle error.\n}\nfor list the ACLs of a bucket or object, obtain an ACLHandle and call its List method:</p>\n<pre><code
_, rule := range acls {\n fmt.Printf(&quot;%s has role %s\\n&quot;, rule.Entity, class=\"prettyprint\">acls, err := obj.ACL().List(ctx)\nif err != nil {\n //
rule.Role)\n}\n</code></pre>\n<p>You can also set and delete ACLs.</p>\n<h3>Conditions</h3>\n<p>Every TODO: Handle error.\n}\nfor _, rule := range acls {\n fmt.Printf(&quot;%s has
object has a generation and a metageneration. The generation changes\nwhenever role %s\\n&quot;, rule.Entity, rule.Role)\n}\n</code></pre><p>You can also set
the content changes, and the metageneration changes whenever the\nmetadata changes. and delete ACLs.</p>\n<h3>Conditions</h3>\n<p>Every object has a generation and
Conditions let you check these values before an operation;\nthe operation only a metageneration. The generation changes\nwhenever the content changes, and the
executes if the conditions match. You can use conditions to\nprevent race conditions metageneration changes whenever the\nmetadata changes. Conditions let you check
in read-modify-write operations.</p>\n<p>For example, say you've read an object's these values before an operation;\nthe operation only executes if the conditions
metadata into objAttrs. Now\nyou want to write to that object, but only if its match. You can use conditions to\nprevent race conditions in read-modify-write
contents haven't changed\nsince you read it. Here is how to express that:</p>\n<pre><code>w operations.</p>\n<p>For example, say you've read an object's metadata into objAttrs.
Now\nyou want to write to that object, but only if its contents haven't changed\nsince
you read it. Here is how to express that:</p>\n<pre><code class=\"prettyprint\">w
= obj.If(storage.Conditions{GenerationMatch: objAttrs.Generation}).NewWriter(ctx)\n// = obj.If(storage.Conditions{GenerationMatch: objAttrs.Generation}).NewWriter(ctx)\n//
Proceed with writing as above.\n</code></pre>\n<h3>Signed URLs</h3>\n<p>You can Proceed with writing as above.\n</code></pre><h3>Signed URLs</h3>\n<p>You can
obtain a URL that lets anyone read or write an object for a limited time.\nYou obtain a URL that lets anyone read or write an object for a limited time.\nYou
don't need to create a client to do this. See the documentation of\nSignedURL don't need to create a client to do this. See the documentation of\nSignedURL
for details.</p>\n<pre><code>url, err := storage.SignedURL(bucketName, &quot;shared-object&quot;, for details.</p>\n<pre><code class=\"prettyprint\">url, err := storage.SignedURL(bucketName,
opts)\nif err != nil {\n // TODO: Handle error.\n}\nfmt.Println(url)\n</code></pre>\n<h3>Post &quot;shared-object&quot;, opts)\nif err != nil {\n // TODO: Handle error.\n}\nfmt.Println(url)\n</code></pre><h3>Post
Policy V4 Signed Request</h3>\n<p>A type of signed request that allows uploads Policy V4 Signed Request</h3>\n<p>A type of signed request that allows uploads
through HTML forms directly to Cloud Storage with\ntemporary permission. Conditions through HTML forms directly to Cloud Storage with\ntemporary permission. Conditions
can be applied to restrict how the HTML form is used and exercised\nby a user.</p>\n<p>For can be applied to restrict how the HTML form is used and exercised\nby a user.</p>\n<p>For
more information, please see https://cloud.google.com/storage/docs/xml-api/post-object more information, please see https://cloud.google.com/storage/docs/xml-api/post-object
as well\nas the documentation of GenerateSignedPostPolicyV4.</p>\n<pre><code>pv4, as well\nas the documentation of GenerateSignedPostPolicyV4.</p>\n<pre><code class=\"prettyprint\">pv4,
err := storage.GenerateSignedPostPolicyV4(bucketName, objectName, opts)\nif err err := storage.GenerateSignedPostPolicyV4(bucketName, objectName, opts)\nif err
!= nil {\n // TODO: Handle error.\n}\nfmt.Printf(&quot;URL: %s\\nFields; %v\\n&quot;, != nil {\n // TODO: Handle error.\n}\nfmt.Printf(&quot;URL: %s\\nFields; %v\\n&quot;,
pv4.URL, pv4.Fields)\n</code></pre>\n<h3>Errors</h3>\n<p>Errors returned by this pv4.URL, pv4.Fields)\n</code></pre><h3>Errors</h3>\n<p>Errors returned by this
client are often of the type <a href=\"https://godoc.org/google.golang.org/api/googleapi#Error\"><code>googleapi.Error</code></a>.\nThese client are often of the type <a href=\"https://godoc.org/google.golang.org/api/googleapi#Error\"><code>googleapi.Error</code></a>.\nThese
errors can be introspected for more information by type asserting to the richer errors can be introspected for more information by type asserting to the richer
<code>googleapi.Error</code> type. For example:</p>\n<pre><code>if e, ok := err.(*googleapi.Error); <code>googleapi.Error</code> type. For example:</p>\n<pre><code class=\"prettyprint\">if
ok {\n\t if e.Code == 409 { ... }\n}\n</code></pre>\n" e, ok := err.(*googleapi.Error); ok {\n\t if e.Code == 409 { ... }\n}\n</code></pre>"
type: package type: package
langs: langs:
- go - go
Expand Down

0 comments on commit 6e49f21

Please sign in to comment.