From 6e49f2148b116ee439c8a882dcfeefb6e7647c57 Mon Sep 17 00:00:00 2001 From: Tyler Bui-Palsulich <26876514+tbpg@users.noreply.github.com> Date: Tue, 16 Mar 2021 16:16:02 -0400 Subject: [PATCH] feat(internal/godocfx): add prettyprint class to code blocks (#3819) Fixes #3804. --- .../godocfx/goldmark-codeblock/codeblock.go | 74 +++++++++++ internal/godocfx/parse.go | 3 +- internal/godocfx/testdata/golden/index.yml | 122 +++++++++--------- 3 files changed, 139 insertions(+), 60 deletions(-) create mode 100644 internal/godocfx/goldmark-codeblock/codeblock.go diff --git a/internal/godocfx/goldmark-codeblock/codeblock.go b/internal/godocfx/goldmark-codeblock/codeblock.go new file mode 100644 index 00000000000..9037bb6e5f0 --- /dev/null +++ b/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(`
`)
+
+		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("
") + 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), + )) +} diff --git a/internal/godocfx/parse.go b/internal/godocfx/parse.go index c5f4d4d4574..5b9f09f6e75 100644 --- a/internal/godocfx/parse.go +++ b/internal/godocfx/parse.go @@ -38,6 +38,7 @@ import ( "strconv" "strings" + goldmarkcodeblock "cloud.google.com/go/internal/godocfx/goldmark-codeblock" "cloud.google.com/go/third_party/go/doc" "cloud.google.com/go/third_party/pkgsite" "github.com/yuin/goldmark" @@ -581,7 +582,7 @@ func toHTML(s string) string { doc.ToMarkdown(buf, s, nil) // 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{} if err := md.Convert(buf.Bytes(), mdBuf); err != nil { panic(err) diff --git a/internal/godocfx/testdata/golden/index.yml b/internal/godocfx/testdata/golden/index.yml index 6dc8c6cb3d6..8d127e93adf 100644 --- a/internal/godocfx/testdata/golden/index.yml +++ b/internal/godocfx/testdata/golden/index.yml @@ -12,82 +12,86 @@ items: as described in\nhttps://cloud.google.com/storage/docs/exponential-backoff. Retrying continues\nindefinitely unless the controlling context is canceled or the client is closed. See\ncontext.WithTimeout and context.WithCancel.

\n

Creating - a Client

\n

To start working with this package, create a client:

\n
ctx
-    := context.Background()\nclient, err := storage.NewClient(ctx)\nif err != nil
-    {\n    // TODO: Handle error.\n}\n
\n

The client will use your default - application credentials. Clients should be\nreused instead of created as needed. - The methods of Client are safe for\nconcurrent use by multiple goroutines.

\n

If + a Client\n

To start working with this package, create a client:

\n
ctx := context.Background()\nclient, err := storage.NewClient(ctx)\nif
+    err != nil {\n    // TODO: Handle error.\n}\n

The client will use + your default application credentials. Clients should be\nreused instead of created + as needed. The methods of Client are safe for\nconcurrent use by multiple goroutines.

\n

If you only wish to access public data, you can create\nan unauthenticated client - with

\n
client, err := storage.NewClient(ctx, option.WithoutAuthentication())\n
\n

Buckets

\n

A - Google Cloud Storage bucket is a collection of objects. To work with a\nbucket, - make a bucket handle:

\n
bkt := client.Bucket(bucketName)\n
\n

A + with

\n
client, err := storage.NewClient(ctx,
+    option.WithoutAuthentication())\n

Buckets

\n

A Google Cloud + Storage bucket is a collection of objects. To work with a\nbucket, make a bucket + handle:

\n
bkt := client.Bucket(bucketName)\n

A 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:

\n
if
-    err := bkt.Create(ctx, projectID, nil); err != nil {\n    // TODO: Handle error.\n}\n
\n

Note - that although buckets are associated with projects, bucket names are\nglobal across - all projects.

\n

Each bucket has associated metadata, represented in this - package by\nBucketAttrs. The third argument to BucketHandle.Create allows you - to set\nthe initial BucketAttrs of a bucket. To retrieve a bucket's attributes, - use\nAttrs:

\n
attrs, err := bkt.Attrs(ctx)\nif err != nil {\n    //
-    TODO: Handle error.\n}\nfmt.Printf("bucket %s, created at %s, is located
-    in %s with storage class %s\\n",\n    attrs.Name, attrs.Created, attrs.Location,
-    attrs.StorageClass)\n
\n

Objects

\n

An object holds arbitrary - data as a sequence of bytes, like a file. You\nrefer to objects using a handle, - just as with buckets, but unlike buckets\nyou don't explicitly create an object. - Instead, the first time you write\nto an object it will be created. You can use - the standard Go io.Reader\nand io.Writer interfaces to read and write object data:

\n
obj
-    := bkt.Object("data")\n// Write something to obj.\n// w implements io.Writer.\nw
-    := obj.NewWriter(ctx)\n// Write some text to obj. This will either create the
-    object or overwrite whatever is there already.\nif _, err := fmt.Fprintf(w, "This
-    object contains text.\\n"); err != nil {\n    // TODO: Handle error.\n}\n//
-    Close, just like writing a file.\nif err := w.Close(); err != nil {\n    // TODO:
-    Handle error.\n}\n\n// Read it back.\nr, err := obj.NewReader(ctx)\nif err !=
-    nil {\n    // TODO: Handle error.\n}\ndefer r.Close()\nif _, err := io.Copy(os.Stdout,
-    r); err != nil {\n    // TODO: Handle error.\n}\n// Prints "This object contains
-    text."\n
\n

Objects also have attributes, which you can fetch - with Attrs:

\n
objAttrs, err := obj.Attrs(ctx)\nif err != nil {\n
-    \   // TODO: Handle error.\n}\nfmt.Printf("object %s has size %d and can
-    be read using %s\\n",\n    objAttrs.Name, objAttrs.Size, objAttrs.MediaLink)\n
\n

Listing - objects

\n

Listing objects in a bucket is done with the Bucket.Objects method:

\n
query
+    exist yet. To create a bucket in Google Cloud Storage,\ncall Create on the handle:

\n
if err := bkt.Create(ctx, projectID, nil); err != nil {\n
+    \   // TODO: Handle error.\n}\n

Note that although buckets are + associated with projects, bucket names are\nglobal across all projects.

\n

Each + bucket has associated metadata, represented in this package by\nBucketAttrs. The + third argument to BucketHandle.Create allows you to set\nthe initial BucketAttrs + of a bucket. To retrieve a bucket's attributes, use\nAttrs:

\n
attrs,
+    err := bkt.Attrs(ctx)\nif err != nil {\n    // TODO: Handle error.\n}\nfmt.Printf("bucket
+    %s, created at %s, is located in %s with storage class %s\\n",\n    attrs.Name,
+    attrs.Created, attrs.Location, attrs.StorageClass)\n

Objects

\n

An + object holds arbitrary data as a sequence of bytes, like a file. You\nrefer to + objects using a handle, just as with buckets, but unlike buckets\nyou don't explicitly + create an object. Instead, the first time you write\nto an object it will be created. + You can use the standard Go io.Reader\nand io.Writer interfaces to read and write + object data:

\n
obj := bkt.Object("data")\n//
+    Write something to obj.\n// w implements io.Writer.\nw := obj.NewWriter(ctx)\n//
+    Write some text to obj. This will either create the object or overwrite whatever
+    is there already.\nif _, err := fmt.Fprintf(w, "This object contains text.\\n");
+    err != nil {\n    // TODO: Handle error.\n}\n// Close, just like writing a file.\nif
+    err := w.Close(); err != nil {\n    // TODO: Handle error.\n}\n\n// Read it back.\nr,
+    err := obj.NewReader(ctx)\nif err != nil {\n    // TODO: Handle error.\n}\ndefer
+    r.Close()\nif _, err := io.Copy(os.Stdout, r); err != nil {\n    // TODO: Handle
+    error.\n}\n// Prints "This object contains text."\n

Objects + also have attributes, which you can fetch with Attrs:

\n
objAttrs,
+    err := obj.Attrs(ctx)\nif err != nil {\n    // TODO: Handle error.\n}\nfmt.Printf("object
+    %s has size %d and can be read using %s\\n",\n    objAttrs.Name, objAttrs.Size,
+    objAttrs.MediaLink)\n

Listing objects

\n

Listing objects + in a bucket is done with the Bucket.Objects method:

\n
query
     := &storage.Query{Prefix: ""}\n\nvar names []string\nit := bkt.Objects(ctx,
     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,
-    attrs.Name)\n}\n
\n

If only a subset of object attributes is needed + attrs.Name)\n}\n

If only a subset of object attributes is needed when listing, specifying this\nsubset using Query.SetAttrSelection may speed up - the listing process:

\n
query := &storage.Query{Prefix: ""}\nquery.SetAttrSelection([]string{"Name"})\n\n//
-    ... as before\n
\n

ACLs

\n

Both objects and buckets have - ACLs (Access Control Lists). An ACL is a list of\nACLRules, each of which specifies - the role of a user, group or project. ACLs\nare suitable for fine-grained control, - but you may prefer using IAM to control\naccess at the project level (see\nhttps://cloud.google.com/storage/docs/access-control/iam).

\n

To - list the ACLs of a bucket or object, obtain an ACLHandle and call its List method:

\n
acls,
-    err := obj.ACL().List(ctx)\nif err != nil {\n    // TODO: Handle error.\n}\nfor
-    _, rule := range acls {\n    fmt.Printf("%s has role %s\\n", rule.Entity,
-    rule.Role)\n}\n
\n

You can also set and delete ACLs.

\n

Conditions

\n

Every - object has a generation and a metageneration. The generation changes\nwhenever - the content changes, and the metageneration changes whenever the\nmetadata changes. - Conditions let you check these values before an operation;\nthe operation only - executes if the conditions match. You can use conditions to\nprevent race conditions - in read-modify-write operations.

\n

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:

\n
w
+    the listing process:

\n
query := &storage.Query{Prefix:
+    ""}\nquery.SetAttrSelection([]string{"Name"})\n\n// ... as
+    before\n

ACLs

\n

Both objects and buckets have ACLs (Access + Control Lists). An ACL is a list of\nACLRules, each of which specifies the role + of a user, group or project. ACLs\nare suitable for fine-grained control, but + you may prefer using IAM to control\naccess at the project level (see\nhttps://cloud.google.com/storage/docs/access-control/iam).

\n

To + list the ACLs of a bucket or object, obtain an ACLHandle and call its List method:

\n
acls, err := obj.ACL().List(ctx)\nif err != nil {\n    //
+    TODO: Handle error.\n}\nfor _, rule := range acls {\n    fmt.Printf("%s has
+    role %s\\n", rule.Entity, rule.Role)\n}\n

You can also set + and delete ACLs.

\n

Conditions

\n

Every object has a generation and + a metageneration. The generation changes\nwhenever the content changes, and the + metageneration changes whenever the\nmetadata changes. Conditions let you check + these values before an operation;\nthe operation only executes if the conditions + match. You can use conditions to\nprevent race conditions in read-modify-write + operations.

\n

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:

\n
w
     = obj.If(storage.Conditions{GenerationMatch: objAttrs.Generation}).NewWriter(ctx)\n//
-    Proceed with writing as above.\n
\n

Signed URLs

\n

You can + Proceed with writing as above.\n

Signed URLs

\n

You can 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 - for details.

\n
url, err := storage.SignedURL(bucketName, "shared-object",
-    opts)\nif err != nil {\n    // TODO: Handle error.\n}\nfmt.Println(url)\n
\n

Post + for details.

\n
url, err := storage.SignedURL(bucketName,
+    "shared-object", opts)\nif err != nil {\n    // TODO: Handle error.\n}\nfmt.Println(url)\n

Post Policy V4 Signed Request

\n

A type of signed request that allows uploads 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.

\n

For more information, please see https://cloud.google.com/storage/docs/xml-api/post-object - as well\nas the documentation of GenerateSignedPostPolicyV4.

\n
pv4,
+    as well\nas the documentation of GenerateSignedPostPolicyV4.

\n
pv4,
     err := storage.GenerateSignedPostPolicyV4(bucketName, objectName, opts)\nif err
     != nil {\n    // TODO: Handle error.\n}\nfmt.Printf("URL: %s\\nFields; %v\\n",
-    pv4.URL, pv4.Fields)\n
\n

Errors

\n

Errors returned by this + pv4.URL, pv4.Fields)\n

Errors

\n

Errors returned by this client are often of the type googleapi.Error.\nThese errors can be introspected for more information by type asserting to the richer - googleapi.Error type. For example:

\n
if e, ok := err.(*googleapi.Error);
-    ok {\n\t  if e.Code == 409 { ... }\n}\n
\n" + googleapi.Error type. For example:

\n
if
+    e, ok := err.(*googleapi.Error); ok {\n\t  if e.Code == 409 { ... }\n}\n
" type: package langs: - go