-
Notifications
You must be signed in to change notification settings - Fork 2k
Markdown Preview sample #1752
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Markdown Preview sample #1752
Changes from 58 commits
9d64ae1
64ab35f
00451bf
86b0635
5132e26
a176376
70ce8f7
5975b57
8e9ef37
867eb65
62fd173
02dccb6
9ef850f
a56dca4
0bafa82
bb14175
1905b18
8353e52
039663d
39dfd1f
5985740
0d0e2ef
7f32b81
f43a2af
2db15ae
7706dd1
0cdf727
1d17988
5fb25b1
43bb924
bea221b
5277daf
a359786
4391a1b
d2410d8
f2fe58e
54649d6
88dd67d
d70d1a6
a8952fa
f2706f0
bca1bb3
4890729
5de6e9c
c97f02b
6c0897c
8842611
b81c140
5a1d84e
971bff9
3c4ed75
f4d8a99
330206e
9f23945
8a0aa37
6bdf2b6
42c6eb8
675b19f
3503649
0bbd133
d5fff16
5a7a2ec
f369ccc
394dc63
618b88e
a7bfaae
2b2844c
eb6bbf1
a2ca8b5
13fb1a0
3084e6f
829039b
b7a7eaa
0a6e84c
719b304
5cfb4e6
59e9e13
77d997c
9f9e9bd
258b7f2
ef89135
4fbb5f4
b071dfb
965b1b6
6bfc88b
0d2d33a
4658d62
f449c88
9cc0974
f8483e2
ef43218
ce5ff21
0806459
e3b2703
bc1b999
1a174f0
71eafed
45e4ca4
9ed8ef5
44af029
c4a152f
35e5795
57aa5ee
9d2028a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # Format: //devtools/kokoro/config/proto/build.proto | ||
|
|
||
| # Set the folder in which the tests are run | ||
| env_vars: { | ||
| key: "PROJECT" | ||
| value: "run/markdown-preview/editor" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # Format: //devtools/kokoro/config/proto/build.proto | ||
|
|
||
| # Set the folder in which the tests are run | ||
| env_vars: { | ||
| key: "PROJECT" | ||
| value: "run/markdown-preview/renderer" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # Cloud Run Markdown Preview Sample | ||
|
|
||
| [Securing Cloud Run services tutorial](https://cloud.google.com/run/docs/tutorials/secure-services) walks through how to create a secure two-service application running on Cloud Run. This application is a Markdown editor which includes a public "frontend" service which anyone can use to compose Markdown text and a private "backend" service which renders Markdown text to HTML. | ||
|
|
||
| For more details on how to work with this sample read the [Google Cloud Run Node.js Samples README](https://github.com/GoogleCloudPlatform/nodejs-docs-samples/tree/master/run). | ||
|
|
||
| ## Environment Variables | ||
|
|
||
| Cloud Run services can be [configured with Environment Variables](https://cloud.google.com/run/docs/configuring/environment-variables). | ||
| Required variables for this sample include: | ||
|
|
||
| * `EDITOR_UPSTREAM_RENDER_URL`: The URL of the restricted Cloud Run service that | ||
| renders Markdown to HTML. | ||
|
|
||
| * `EDITOR_UPSTREAM_UNAUTHENTICATED`: (Optional) A boolean that indicates whether the render service requires an authenticated request. | ||
|
|
||
| ## Dependencies | ||
|
|
||
| * **express**: Web server framework. | ||
| * **gcp-metadata**: Access Google Cloud Platform metadata server. | ||
|
||
| * **got**: Node.js library for HTTP requests. | ||
| * **handlebars** JavaScript template engine. | ||
| * **markdown-it**: JavaScript library for parsing and rendering Markdown text. | ||
| * **xss**: Node.js HTML sanitizer. | ||
kelsk marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| # Copyright 2020 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 | ||
| # | ||
| # https://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. | ||
|
|
||
| # Use the official lightweight Node.js 10 image. | ||
| # https://hub.docker.com/_/node | ||
| FROM node:10-slim | ||
|
||
|
|
||
| # Create and change to the app directory. | ||
| WORKDIR /usr/src/app | ||
|
|
||
| # Copy application dependency manifests to the container image. | ||
| # A wildcard is used to ensure both package.json AND package-lock.json are copied. | ||
| # Copying this separately prevents re-running npm install on every code change. | ||
| COPY package*.json ./ | ||
|
|
||
| # Install production dependencies. | ||
| RUN npm install --only=production | ||
|
|
||
| # Copy local code to the container image. | ||
| COPY . ./ | ||
|
|
||
| # Run the web service on container startup. | ||
| CMD [ "npm", "start" ] | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,81 @@ | ||||||
| // Copyright 2020 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 | ||||||
| // | ||||||
| // https://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. | ||||||
|
|
||||||
| const express = require('express'); | ||||||
| const handlebars = require('handlebars'); | ||||||
| const { readFileSync } = require('fs'); | ||||||
|
||||||
| const renderRequest = require('./render.js'); | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One of the reasons why the golang code was structured the way it is: I was hiding from the main flow of the program that the rendering process was an external API call. In fact, from the perspective of two of the files, I could re-implement the rendering to be wholly local and they wouldn't be able to tell. We don't need to do that in node.js, but it's worth considering how that affects the shape of the code if you aren't trying to abstract things. For example, should the functions in |
||||||
|
|
||||||
| const app = express(); | ||||||
| app.use(express.json()); | ||||||
|
|
||||||
| let url, isAuthenticated, markdownDefault, compiledTemplate, template; | ||||||
|
|
||||||
| const init = () => { | ||||||
|
||||||
| url = process.env.EDITOR_UPSTREAM_RENDER_URL; | ||||||
| if (!url) throw Error ("No configuration for upstream render service: add EDITOR_UPSTREAM_RENDER_URL environment variable"); | ||||||
| isAuthenticated = !process.env.EDITOR_UPSTREAM_UNAUTHENTICATED; | ||||||
| if (!isAuthenticated) console.log("Editor: starting in unauthenticated upstream mode"); | ||||||
| return {url, isAuthenticated}; | ||||||
|
||||||
| }; | ||||||
|
|
||||||
| const service = init(); | ||||||
|
|
||||||
| // Load the template files and serve them with the Editor service. | ||||||
| const buildTemplate = () => { | ||||||
|
||||||
| try { | ||||||
| markdownDefault = readFileSync(__dirname + '/templates/markdown.md'); | ||||||
| const indexTemplate = handlebars.compile(readFileSync(__dirname + '/templates/index.html', 'utf8')); | ||||||
|
||||||
| compiledTemplate = indexTemplate({default: markdownDefault}); | ||||||
|
||||||
| return compiledTemplate; | ||||||
| } catch(err) { | ||||||
| throw Error ('Error loading template: ', err); | ||||||
| } | ||||||
| }; | ||||||
|
|
||||||
| app.get('/', (req, res) => { | ||||||
| try { | ||||||
| template = buildTemplate(); | ||||||
|
||||||
| template = buildTemplate(); | |
| if (!template) template = buildTemplate(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Understood, and updated. Using 'template' as the conditional variable to determine whether to run buildTemplate() makes the most sense to me, as opposed to creating an arbitrary parameter to determine whether the template should be rendered again.
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noted in comments on buildTemplate() that this is not currently the template, but the rendered HTML. We wouldn't actually return the template. Reinforcing it in both places for clarity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updating indexTemplate, compiledTemplate, template, and buildTemplate() to the following variable/function names:
indexTemplate => compiledTemplate
compiledTemplate => renderedHtml
template => renderedHtml
buildTemplate() => buildRenderedHtml()
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| { | ||
| "name": "markdown-preview", | ||
| "description": "Cloud Run service to demonstrate service-to-service authentication, paired with Renderer service.", | ||
| "version": "0.0.1", | ||
| "private": true, | ||
| "license": "Apache-2.0", | ||
| "author": "Google LLC", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" | ||
| }, | ||
| "engines": { | ||
| "node": ">= 10.0.0" | ||
|
||
| }, | ||
| "main": "main.js", | ||
| "scripts": { | ||
| "start": "node main.js", | ||
| "test": "mocha test/*.test.js --exit" | ||
|
||
| }, | ||
| "dependencies": { | ||
| "express": "^4.17.1", | ||
| "gcp-metadata": "^4.0.0", | ||
| "got": "^10.7.0", | ||
| "handlebars": "^4.7.6" | ||
| }, | ||
| "devDependencies": { | ||
| "mocha": "^7.1.1", | ||
| "sinon": "^9.0.2", | ||
| "supertest": "^4.0.2" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| // Copyright 2020 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 | ||
| // | ||
| // https://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. | ||
|
|
||
| const gcpMetadata = require('gcp-metadata') | ||
| const got = require('got'); | ||
|
|
||
| // renderRequest creates a new HTTP request with IAM ID Token credential. | ||
| // This token is automatically handled by private Cloud Run (fully managed) and Cloud Functions. | ||
| const renderRequest = async (service, markdown) => { | ||
| // [START run_secure_request] | ||
| let token; | ||
|
|
||
| // Build the request to the Renderer receiving service. | ||
| const serviceRequestOptions = { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'text/plain' | ||
| }, | ||
| body: markdown, | ||
| timeout: 3000 | ||
| }; | ||
|
|
||
| if (service.isAuthenticated) { | ||
| try { | ||
| // Query the token with ?audience as the service URL. | ||
| const metadataServerTokenPath = `service-accounts/default/identity?audience=${service.url}`; | ||
|
||
| // Fetch the token and then add it to the request header. | ||
| token = await gcpMetadata.instance(metadataServerTokenPath); | ||
| serviceRequestOptions.headers['Authorization'] = 'bearer ' + token; | ||
| } catch(err) { | ||
| throw Error('Metadata server could not respond to request: ', err); | ||
| } | ||
| }; | ||
| // [END run_secure_request] | ||
|
|
||
| // [START run_secure_request_do] | ||
| try { | ||
| // serviceRequest converts the Markdown plaintext to HTML. | ||
| const serviceResponse = await got(service.url, serviceRequestOptions); | ||
| return serviceResponse.body; | ||
| } catch (err) { | ||
| throw Error('Renderer service could not respond to request: ', err); | ||
| }; | ||
| // [END run_secure_request_do] | ||
|
||
| }; | ||
|
|
||
| module.exports = renderRequest; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| <!-- | ||
| Copyright 2020 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. | ||
| --> | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8"> | ||
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | ||
| <title>Markdown Editor</title> | ||
| <link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo="> | ||
| <link href="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.css" rel="stylesheet"> | ||
| <script src="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.js"></script> | ||
| <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"> | ||
| </head> | ||
| <body class="mdc-typography"> | ||
|
|
||
| <header class="mdc-top-app-bar mdc-top-app-bar--fixed"> | ||
| <div class="mdc-top-app-bar__row"> | ||
| <section class="mdc-top-app-bar__section mdc-top-app-bar__section--align-start"> | ||
| <span class="mdc-top-app-bar__title">Markdown Editor</span> | ||
| </section> | ||
| <section class="mdc-top-app-bar__section mdc-top-app-bar__section--align-end" role="toolbar"> | ||
| <a href="#code" title="View the code"><i class="material-icons mdc-top-app-bar__action-item mdc-icon-button" aria-hidden="true">code</i></a> | ||
| <a href="#tutorial" title="Read the tutorial"><i class="material-icons mdc-top-app-bar__action-item mdc-icon-button" aria-hidden="true">assignment</i></a> | ||
| </section> | ||
| </div> | ||
| </header> | ||
|
|
||
| <div role="progressbar" class="mdc-linear-progress mdc-linear-progress--indeterminate mdc-top-app-bar--fixed-adjust" aria-label="Markdown Rendering Progress Bar" aria-valuemin="0" aria-valuemax="1" aria-valuenow="0"> | ||
| <div class="mdc-linear-progress__bar mdc-linear-progress__primary-bar"> | ||
| <span class="mdc-linear-progress__bar-inner"></span> | ||
| </div> | ||
| </div> | ||
|
|
||
| <main class="mdc-layout-grid"> | ||
| <div class="mdc-layout-grid__inner"> | ||
| <div class="mdc-layout-grid__cell mdc-layout-grid__cell--span-6"> | ||
| <h2>Markdown Text</h2> | ||
| <section class="mdc-card mdc-card--outlined"> | ||
| <div class="text-field-container"> | ||
| <div class="mdc-text-field mdc-text-field--fullwidth md-text-field--no-label mdc-text-field--textarea mdc-ripple-upgraded"> | ||
| <textarea id="editor" class="mdc-text-field__input" style="height: 36rem;">{{ default }}</textarea> | ||
| </div></div> | ||
|
|
||
| <div class="mdc-card__actions mdc-card__actions--full-bleed"> | ||
| <button class="editor-button mdc-button mdc-card__action mdc-card__action--button mdc-ripple-surface"> | ||
| <span class="mdc-button__label">Preview Rendered Markdown</span> | ||
| <i class="material-icons" aria-hidden="true">arrow_forward</i> | ||
| </button> | ||
| </div> | ||
| </section></div> | ||
|
|
||
| <div class="mdc-layout-grid__cell mdc-layout-grid__cell--span-6"> | ||
| <h2>Rendered HTML</h2> | ||
| <section class="mdc-card mdc-card--outlined"> | ||
| <div id="preview" style="height: 40rem; padding-left: 10px; padding-right: 10px">Tap "<strong>Preview Rendered Markdown</strong>" below the text entry to see rendered content.</div> | ||
| </section></div> | ||
| </div> | ||
| </div> | ||
| </main> | ||
|
|
||
| <script> | ||
| const preview = document.getElementById('preview'); | ||
| const lp = new mdc.linearProgress.MDCLinearProgress(document.querySelector('.mdc-linear-progress')); | ||
| async function render(data = {}) { | ||
| const response = await fetch('/render', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json' | ||
| }, | ||
| body: JSON.stringify(data) | ||
| }); | ||
|
|
||
| const text = await response.text(); | ||
| if (!response.ok) { | ||
| console.log('error: Render Text: Received status code: ' + response.status); | ||
| } | ||
|
|
||
| return text; | ||
| } | ||
|
|
||
| function listener() { | ||
| lp.open(); | ||
| render({data: document.getElementById('editor').value}) | ||
| .then((result) => preview.innerHTML = result) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are we mixing
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @grayside thoughts on updating this?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, we should fix this to use await wherever possible and backtrack to the other languages.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, we should fix this to use await wherever possible and backtrack to the other languages.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, we should fix this to use await wherever possible and backtrack to the other languages. |
||
| .catch((err) => { | ||
| console.log('Render Text: ' + err.message); | ||
| preview.innerHTML = '<h3><i aria-hidden="true" class="material-icons">error</i>Render Error</h3>\n<p>' + err.message + '</p>'; | ||
| }) | ||
| .finally(() => lp.close()) | ||
| } | ||
|
|
||
| document.querySelector('.editor-button').addEventListener('click', listener); | ||
| window.addEventListener('load', listener); | ||
| </script> | ||
| </body> | ||
| </html> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| # Playing with Markdown | ||
|
|
||
| This UI allows a user to write Markdown text and preview the rendered HTML. | ||
|
|
||
| You may be familiar with this workflow from sites such as Github or Wikipedia. | ||
|
|
||
| In practice, this web page does the following: | ||
|
|
||
| * On click of the *"Preview Rendered Markdown"* button, browser JavaScript | ||
| lifts the markdown text and sends it to the editor UI's public backend. | ||
| * The editor backend sends the text to a private Renderer service which | ||
| converts it to HTML. | ||
| * The HTML is injected into the web page in the right-side **Rendered HTML** area. | ||
|
|
||
| ## Markdown Background | ||
|
|
||
| Markdown is a text-to-HTML conversion tool that allows you to convert plain text to valid HTML. | ||
|
|
||
| Read more about the [syntax on Wikipedia](https://en.wikipedia.org/wiki/Markdown). |
Uh oh!
There was an error while loading. Please reload this page.