DEV Community

Wesley Chun (@wescpy) for Google Workspace Developers

Posted on • Edited on

Building a basic Markdown-to-Google Docs converter

Primer on using the Docs #API... TL;DR:

Other Google Workspace (GWS) API posts featured on this blog seem to be about Docs & Sheets (well, their APIs), but the code samples are almost always file-oriented operations, like exporting Google Docs as PDF or importing CSV files into Google Sheets. File-related activity generally points to the Google Drive API instead of "editor" APIs. This post breaks free from that pattern, kicking off a Markdown-to-Google Docs converter; yes, using the Docs (not Drive) API! From this post, readers will know how to programmatically create new Google Docs, write text to them, and format text in them.

Markdown to Google Docs conversion

Introduction and motivation

You've stumbled on the blog focused on showing Python (and sometimes Node.js) developers how to use different parts of Google's developer ecosystem, from APIs to compute and AI/ML platforms where I provide the "oil" to smoothen your onboarding friction. Pick any topic, and I probably have you covered:

I have a "problem" with most of my GWS content: they only cover the Drive API. Boring! Workspace has so many others, I'm not doing any of you any favors if I don't explore another GWS API. I randomly chose the Google Docs API for this post.

Application: inspiration & motivation

The sample app is inspired by a pair of projects familiar to me. A while back, I created a tool that ingests & parses the outline of a presentation in a Markdown-like syntax, converting it into a Microsoft PowerPoint presentation. You could even set the template and kick off the slide show! That script was soon joined by others for Microsoft Word, Excel, Outlook, and covered in Chapter 7 ("Programming Microsoft Office") of one of my Python books. (To fans and readers: Yes, I know I need to work on a new edition and get the code into GitHub!)

After launching the Google Slides API years later, I wanted to create a similar tool for Slides, however a colleague beat me to it with md2googleslides, a well-forked & starred project that supports full Markdown and a variety of content. A huge app like that would be overwhelming for short posts like these, so I settled on a very rudimentary, the most basic, subset of Markdown, supporting only underscores (_) for italics and asterisks (*) for bold:

Take a file formatted with any of those directives, parse out the text from the Markdown, generate a Google Doc document, insert the text, then format it per the markup directives. (Other incarnations of Markdown or Wiki formatting use a single symbol [either one] for italics and double for bold, however I opted for a simpler implementation for regex purposes.)

While you can compose in Markdown with Docs as well as import & export Markdown in Docs, you don't want to really use the UI (user interface) to process hundreds or thousands of generated Markdown files. To truly automate and scale beyond "one file at a time," you need API power.

📝 NOTE: Alternate version uses older Python (OAuth2) auth library
There is an alternate version of the app in this post: the primary script uses the current Google auth library for Python while the alternative (named *-old.py) utilizes the older auth library which was deprecated in 2017 but is still widely used (and unfortunately, with many samples online). It serves as a migration tool to help remaining users upgrade.
📝 NOTE: Code sample Python 2 and 3 compatible
In a similar vein to help Python 2 users upgrade & migrate to Python 3, the sample scripts are both 2.x- and 3.x-compatible. However, this means you won't find 3-only features like f-strings, type annotations, or async & await. If a reader wants to submit a pure Python 3 version, I'm open to entertaining PR requests.

The code

The sample app comes in four (4) flavors, a pair each in Python and Node.js, all accessible in the repo folder.

Imports

The md2docs.py import code at the top look like this:

Python

from __future__ import print_function
import os.path
import re

from google.auth.transport.requests import Request
from google.oauth2 import credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient import discovery
Enter fullscreen mode Exit fullscreen mode

The standard library imports bring Python 3's print() function to Python 2 (ignored in 3.x), filesystem access via os, and re for regular expressions. Those are followed by importing the Google API and auth client libraries.

For developers still using the older auth library, here are the equivalent imports in md2docs-old.py:

from __future__ import print_function
import re

from googleapiclient import discovery
from httplib2 import Http
from oauth2client import file, client, tools
Enter fullscreen mode Exit fullscreen mode

JavaScript
Switching to JavaScript, below are the equivalent lines for the Node.js ES module, md2docs.mjs:

import fs from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';
import { authenticate } from '@google-cloud/local-auth';
import { google } from 'googleapis';
Enter fullscreen mode Exit fullscreen mode

If you prefer CommonJS, that file is md2docs.js, and those lines look like this:

const fs = require('fs').promises;
const path = require('path');
const process = require('process');
const { authenticate } = require('@google-cloud/local-auth');
const { google } = require('googleapis');
Enter fullscreen mode Exit fullscreen mode

Like their Python twins, these employ packages for filesystem operations as well as Google security and API access. This is where the differences end between the JavaScript versions... the remainder of the code is identical across both ES modules and CommonJS.

Security

Next are the constants and security code:

Python

FILENAME = 'quickbrownfox.md'  # change to your own Markdown file

creds = None
SCOPES = 'https://www.googleapis.com/auth/documents'
TOKENS = 'storage.json'
if os.path.exists(TOKENS):
    creds = credentials.Credentials.from_authorized_user_file(TOKENS)
if not (creds and creds.valid):
    if creds and creds.expired and creds.refresh_token:
        creds.refresh(Request())
    else:
        flow = InstalledAppFlow.from_client_secrets_file('client_secret.json', SCOPES)
        creds = flow.run_local_server()
with open(TOKENS, 'w') as token:
    token.write(creds.to_json())
DOCS = discovery.build('docs', 'v1', credentials=creds)
Enter fullscreen mode Exit fullscreen mode

The old auth version is a bit simpler to use because that library manages the OAuth token storage (so you don't have to):

FILENAME = 'quickbrownfox.md'  # change to your own Markdown file

SCOPES = 'https://www.googleapis.com/auth/documents'
store = file.Storage('storage.json')
creds = store.get()
if not creds or creds.invalid:
    flow = client.flow_from_clientsecrets('client_secret.json', SCOPES)
    creds = tools.run_flow(flow, store)
DOCS = discovery.build('docs', 'v1', http=creds.authorize(Http()))
Enter fullscreen mode Exit fullscreen mode

Beyond this, both Python versions are identical. The JavaScript code operates in a similar way as in Python but does it slightly differently: each piece of functionality is broken up into its own function with improved constants & naming:

JavaScript

const FILENAME = 'quickbrownfox.md';  // change to your own Markdown file
const CREDENTIALS_PATH = path.join(process.cwd(), 'client_secret.json');
const TOKEN_STORE_PATH = path.join(process.cwd(), 'storage.json');
const SCOPES = [ 'https://www.googleapis.com/auth/documents' ];

async function loadSavedCredentialsIfExist() {
  try {
    const content = await fs.readFile(TOKEN_STORE_PATH);
    const credentials = JSON.parse(content);
    return google.auth.fromJSON(credentials);
  } catch (err) {
    return null;
  }
}

async function saveCredentials(client) {
  const content = await fs.readFile(CREDENTIALS_PATH);
  const keys = JSON.parse(content);
  const key = keys.installed || keys.web;
  const payload = JSON.stringify({
    type: 'authorized_user',
    client_id: key.client_id,
    client_secret: key.client_secret,
    refresh_token: client.credentials.refresh_token,
    access_token: client.credentials.access_token,
    token_expiry: client.credentials.token_expiry,
    scopes: client.credentials.scopes,
  });
  await fs.writeFile(TOKEN_STORE_PATH, payload);
}

async function authorize() {
  var client = await loadSavedCredentialsIfExist();
  if (client) return client;
  client = await authenticate({
    scopes: SCOPES,
    keyfilePath: CREDENTIALS_PATH,
  });
  if (client.credentials) await saveCredentials(client);
  return client;
}
Enter fullscreen mode Exit fullscreen mode

The constant at the top points to the quickbrownfox.md data file, but its contents are arbitrary. Feel free to either change the content or point the code to your own Markdown file. For demonstration's sake, quickbrownfox.md as provided contains this Markdown data:

The _quick, brown_ fox jumped over the *lazy dogs*.
Enter fullscreen mode Exit fullscreen mode

The security code is required because the end-user must grant their permission for your code to access their data; in this case, creating documents on their behalf. Authorization ("authz") requires you to create a client ID & secret pair, then download the file from your Google developer project.

Complete info on this code segment and how "to do" authz for GWS APIs can be found in the OAuth client ID 3-part series (primarily the last post), so I won't repeat it here. Check them out if you're new or need a refresher. After this code review, I'll provide setup steps for running the samples but check those posts for all the details.

The Python security snippets end with DOCS, a Docs API client object or "service endpoint" to use for Docs API calls. (In many of official Google code samples, rather than a descriptive constant such as DOCS, you'll find service used as the variable name instead and will now know why.) For JavaScript, rather than a global variable, the main driver application manages it and passes it to each function where an API call is needed.

Read and parse Markdown

The next step is to read and parse the Markdown file content.

Python

def read_parse_md(fname):
    with open(fname, 'r') as f:
        content = f.read()
    actions = []
    matches = re.findall(r'(([_*])([^\2]+?)\2)', content)
    for match in matches:
        md, dl, pt = match
        i = content.find(md)
        j = i + len(pt) + 1
        content = content.replace(md, pt)
        action = 'bold' if dl == '*' else 'italic'
        actions.append((action, i, j))
    return content, actions
Enter fullscreen mode Exit fullscreen mode

JavaScript

async function read_parse_md(fname) {
  var contentBuf = await fs.readFile(FILENAME);
  var content = await contentBuf.toString('utf-8');
  var actions = [];
  const matches = await content.matchAll(/(([_*])([^\2]+?)\2)/g);
  for (const match of matches) {
    const [ md, dl, pt ] = await match.slice(1, 4);
    var i = await content.indexOf(md);
    var j = i + pt.length + 1;
    content = await content.replace(md, pt);
    var action = (dl == '*') ? 'bold' : 'italic';
    await actions.push([ action, i, j ]);
  }
  return [ content, actions ];
}
Enter fullscreen mode Exit fullscreen mode

Both samples are nearly line-by-line identical, so let's dive into them together. The read_parse_md() function is as advertised: it reads and parses the Markdown file, converting the markup into formatting actions to request via the Docs API.

A regular expression ("regex") is used to pattern-match strings that need to be italicized with underscores, e.g., _sample_, and those to be bolded, e.g., *sample*. A single pass through the content string takes place, converting each stylization request to an API "styling action."

The regex may be a bit obfuscated to look at, but it basically works like this: look for an opening _ or * and cache that symbol as \2 (the 2nd pattern match). Upon discovering a match, seek its closing twin symbol with the minimal characters grabbed [in a non-greedy (?) way], and save both the entire marked-up (md) and plain text (pt) strings plus the delimiter (dl). For "_sample_", md would be _sample_, dl is _, pt is sample, and action is set to italic. The table below illustrates all these values:

Regex Regex match Saved-to variable Description Example
((...\2) (outermost ()) \1 "MarkDown:" md Entire (single) pattern-matched string _sample_
([_*]) \2 "DeLimiter:" dl Markdown directive _
([^\2]+?) \3 "Plain Text:" pt Pattern-matched string with delimiters stripped sample
Regex patterns & matches

 

The easiest way to "decipher" the match numbers is \1 is the first left parenthesis found in the regex pattern, \2 is the 2nd, and so on.

To tell the Docs API to stylize any piece of text, it must know where the text is, meaning your requests have to provide the location as well as the action. These are raw offsets: a start index (i) and an end index (j). All style requests are processed in this manner, saving (action, i, j) into the actions array.

At the end, any markup will be stripped out and raw content "reduced" to plain text. The actions array has all the "instructions" required for stylizing via the API.

Creating and writing to a Google Doc

Armed with all the content and instructions, we need code to create a new, empty Google Doc and write the data string into it. There is one function for each task:

Function Description
create_doc() Create new/empty Google Doc and return its file ID
write_text() Write text into Doc identified by file ID

Python

def create_doc(fname):
    return DOCS.documents().create(
            body={'title': fname}).execute().get('documentId')

def write_text(text, docs_id):
    requests = [{'insertText': {'location': {'index': 1}, 'text': text}}]
    DOCS.documents().batchUpdate(body={'requests': requests},
            documentId=docs_id, fields='').execute()
Enter fullscreen mode Exit fullscreen mode

JavaScript

async function create_doc(DOCS, fname) {
  const rsp = await DOCS.documents.create({ requestBody: { title: fname } });
  return rsp.data.documentId;
}

async function write_text(DOCS, text, docs_id) {
  const requests = [ { insertText: { location: { index: 1 }, text: text } } ];
  await DOCS.documents.batchUpdate({
    requestBody: { requests: requests },
    documentId: docs_id
  });
}
Enter fullscreen mode Exit fullscreen mode

A document name is all that's needed to create a new Google Doc; in this case, it's the Markdown filename with the .md file extension removed. The create_doc() function issues the request and returns the newly-created document's (Google Drive) file ID.

Updating a Google Doc requires one or more requests. In this case, it's an InsertTextRequest which requires, at minimum, two things:

  1. The text to insert
  2. The location (index offset)

In a new document, inserting at index 1 (not 0) is the right place. The Node.js version of write_text() differs only in that the API client object DOCS is passed into each call whereas the same global variable is used in Python.

Formatting text in a Google Doc

The last piece of the puzzle is a function that formats text in a Google Doc; that's format_text() is for:

Python

def format_text(actions, docs_id):
    'format text with requested Markdown styling'
    requests = [{
        'updateTextStyle': {
            'range': {'startIndex': i, 'endIndex': j},
            'textStyle': {style: True}, 'fields': style,
        }
    } for style, i, j in actions]
    DOCS.documents().batchUpdate(body={'requests': requests},
            documentId=docs_id, fields='').execute()
Enter fullscreen mode Exit fullscreen mode

JavaScript

async function format_text(DOCS, actions, docs_id) {
  const requests = [];
  for (const [ style, i, j ] of actions) {
    requests.push({
      updateTextStyle: {
        range: { startIndex: i, endIndex: j },
        textStyle: { [ style ]: true }, fields: style
      }
    });
  }

  await DOCS.documents.batchUpdate({
      requestBody: { requests: requests },
      documentId: docs_id
  });
}
Enter fullscreen mode Exit fullscreen mode

It takes the document's file ID (docs_id) and the stylization actions (array), and builds an equivalent array of updateTextStyle API requests, meaning a bold or italicize command plus the location of the affected text.

If it "seems" like requests is an array of JSON objects, you're spot on. If you could dump its contents after processing actions, it would look like this:

[{'updateTextStyle': {'fields': 'italic',
                      'range': {'endIndex': 17, 'startIndex': 4},
                      'textStyle': {'italic': True}}},
 {'updateTextStyle': {'fields': 'bold',
                      'range': {'endIndex': 47, 'startIndex': 37},
                      'textStyle': {'bold': True}}}]
Enter fullscreen mode Exit fullscreen mode

The last thing it does is pass the array along with the file ID to the API which executes the requests. This is a very rudimentary processor of the most basic Markdown directives. The point is to get you started on using the Docs API, not writing a fully-featured Markdown parser, which you can pick up as an exercise if desired.

Main driver

While all these functions are great, a "main" driver is required to tie everything together. Here they are respectively:

Python

if __name__ == '__main__':
    text, actions = read_parse_md(FILENAME)
    print('** Parsed Markdown file %r & style actions' % FILENAME)
    docs_fn = FILENAME.replace('.md', '')  # remove MD file ext
    docs_id = create_doc(docs_fn)
    print('** Created %r (ID: %s)' % (docs_fn, docs_id))
    write_text(text, docs_id)
    print('** Inserted %r into document' % text)
    format_text(actions, docs_id)
    print('** Completed Markdown formatting in document:', actions)
Enter fullscreen mode Exit fullscreen mode

JavaScript

async function md2docs(authClient) {
  const DOCS = google.docs({ version: 'v1', auth: authClient });
  const [ text, actions ] = await read_parse_md(FILENAME);
  console.log(`** Parsed Markdown file '${FILENAME}' & style actions`);
  const docs_fn = await FILENAME.replace('.md', '');
  const docs_id = await create_doc(DOCS, docs_fn);
  console.log(`** Created '${docs_fn}' (ID: ${docs_id})`);
  await write_text(DOCS, text, docs_id);
  console.log(`** Inserted '${text.replace(/\n/g, "\\n")}' into document`);
  await format_text(DOCS, actions, docs_id);
  console.log('** Completed Markdown formatting in document:', actions);
}

authorize().then(md2docs).catch(console.error);
Enter fullscreen mode Exit fullscreen mode

Both snippets are nearly identical in functionality, with the only differences being:

  1. In JS, the DOCS API client object is local to this block where Python uses a global.
  2. The last line of JS code runs authorize() and passes the returned authClient object to this main md2docs() function which can catch & log an exception.

Regardless of which language you're using, the recipe of this app is as follows:

  1. Read and parse the Markdown contents
  2. Create a new, empty Google Doc
  3. Write the plain text string into the Doc
  4. Make all the stylization requests in the Doc

Prerequisites/required setup

When running the scripts, the output follows that exact recipe. However, before you can run anything, take these required steps:

  1. Create a new project from the Cloud/developer console or with the gcloud projects create . . . command; alternatively, reuse an existing project.
  2. Enable the Google Docs API. Pick your preferred method of these three common ways to enable APIs:
    • DevConsole manually -- Enable the API manually from the DevConsole by following these steps:
      1. Go to DevConsole
      2. Click on Library tab in the left-nav; search for "Docs", and enable
    • DevConsole link -- You may be new to Google APIs or don't have experience enabling APIs manually in the DevConsole. If this is you...
      1. Check out the API listing page to learn more about the API and enable it from there.
      2. Alternatively, skip the API info and click this link for the enable button.
    • Command-line (gcloud) -- Those who prefer working in a terminal can enable APIs with a single command in the Cloud Shell or locally on your computer if you installed the Cloud SDK which includes the gcloud command-line tool (CLI) and initialized its use.
      1. If this is you, issue this command to enable the API: gcloud services enable docs.googleapis.com
      2. Confirm all the APIs you've enabled with this command: gcloud services list
  3. Create OAuth client ID & secret credentials and download to your local filesystem as client_secret.json. The code samples will not run without this file present.
  4. Install Google APIs client library:
    • NodeJS (16+): Install required packages with this command:
      • npm i (uses package.json)
      • Or install the packages manually: npm i googleapis @google-cloud/local-auth
    • Python 2 or 3 (new auth): In your normal or virtualenv environment, run this command if using the current Python auth libraries (most everyone):
      • pip install -r requirements.txt (or pip3)
      • Or install the packages manually: pip install -U pip google-api-python-client google-auth-httplib2 google-auth-oauthlib
    • Python 2 or 3 (old auth): If you have dependencies on the older Python auth libraries and/or still have old code lying around that do (see warning sidebar above), run this command to ensure you have the latest/last versions of these libraries:
      • pip install -r requirements-old.txt (or pip3)
      • Or install the packages manually: pip install -U pip google-api-python-client oauth2client (or pip3)
    • For Python specifically, 2.x means 2.7, and if you're already planning to migrate to 3.x, you should definitely not be using anything older. For 3.x, it should work for nearly all releases, but 3.9 or newer are recommended.

Once you've done all of the above, you're ready to go. Oh, a quick word about costs running this app: there should be none.

⚠️ ALERT: Cost: "free" up to certain limits
While many Google products & APIs are free to use, not all of them are. While not totally "free," use of GWS APIs is covered completely by your monthly "subscription," whether you're a paid subscriber or have a free consumer Google account (with or without Gmail, which is optional), meaning a $0USD monthly subscription rate.

This "free" usage is not unlimited however... stay within the established quotas for each API. As expected, paid subscribers get more quota than free accounts. While not broadly published, you can get an idea of the limits on the Quotas page for Google Apps Script.

Running the script

Since we know the "recipe" of how this app operates, running them produce expected results, similar to what you see below, with some slight differences in the objects rendered by each language:

Python

$ python md2docs.py
** Parsed Markdown file 'quickbrownfox.md' & style actions
** Created 'quickbrownfox' (ID: bfENGvI9vCE2cabBCqv3Hq6qvFoL0nfDPfQZeZLY6ubQ)
** Inserted 'The quick, brown fox jumped over the lazy dogs.\n' into document
** Completed Markdown formatting in document: [('italic', 4, 17), ('bold', 37, 47)]
Enter fullscreen mode Exit fullscreen mode

JavaScript

$ node md2docs.mjs
** Parsed Markdown file 'quickbrownfox.md' & style actions
** Created 'quickbrownfox' (ID: ATjQg2PqFQxqmiRNIi1TL9gjXBFWV7cw0i4ytGtddt7K)
** Inserted 'The quick, brown fox jumped over the lazy dogs.\n' into document
** Completed Markdown formatting in document: [ [ 'italic', 4, 17 ], [ 'bold', 37, 47 ] ]
Enter fullscreen mode Exit fullscreen mode

If you could freeze the app and open the Google Doc at the exact moment after the plain text is added to the Doc, it would look something like this:

Plain text string inserted into Google Doc

Plain text string inserted into Google Doc

 

However, unless you pause execution, it's an unlikely screenshot because formatting comes quickly thereafter, meaning you'll see the final result instead:

Stylization-Formatted string in Google Docs

Stylization-formatted string in Google Doc

Summary and next steps

Now you know how to create a crudely-simplistic Markdown parser using regexes, but more importantly, learned how to use the Google Docs API to:

  1. Create a new Google Doc
  2. Write text to a Google Doc
  3. Format text in a Google Doc

While the sample serves as a way to kickstart a very modest Markdown parser, the main goals are for readers to become familiar with the Docs API, which hasn't been around for that long (launched in 2017). If you wanted to support more complete Markdown syntax, you'd consider a package like markdown2 for Python or Showdown for Node.js.

If you pair the sample script demonstrated in this post and combine it with the sample in the exporting Google Docs as PDF files post, you'll have a solution that takes auto-generated Markdown files, produces formatted Google Docs, and exports corresponding PDFs for your organization or your customers.

You could also take things a step further: take valuable corporate data sitting in a massive set of CSV files, import them into Google Sheets, then leverage the Gemini API to produce summaries of the content, write out a collection of Google Docs, then produce PDFs for your boss/management, or perhaps even paying customers.

Those are just some ideas. The point of this imagination is to inspire you with what's possible, and what kinds of solutions you can build. I'll cover other GWS APIs in future posts. For fun, I'm also going to do a "codegen" (code generation) post where I ask LLMs ("large language models") like ChatGPT & Gemini to generate an app like this, discuss what I get, and compare/contrast.

Wrap-up

If you found an error in this post, a bug in the code, or have a topic I should cover, drop a note in the comments below or file an issue at the repo. I enjoy meeting users on the road... see if I'll be visiting your community in the travel calendar on my consulting page.

References

Below are various resources related to this post which you may find useful.

Code samples

Google Docs API

GWS APIs & OAuth2 information

Google APIs client libraries

Other relevant content by the author

  • GWS APIs specific use cases
    • Mail merge with the Google Docs API post & video
    • Exporting Google Docs as PDF post
    • Importing CSV files into Google Sheets post
  • GWS APIs intro content (featuring Drive API)
  • GWS APIs general
    • Using OAuth Client IDs & GWS APIs 3-part post series
    • GWS/G Suite developer overview post & video (open to all but originally for students)
    • Accessing GWS/G Suite REST APIs post & video (open to all but originally for students)
    • Power your apps with Gmail, Drive, Docs, Sheets, Slides (G Suite/GWS comprehensive developer overview) video (LONG)
  • GWS APIs video series
  • Google APIs general
    • Getting started with Google APIs post & video
    • Python authorization boilerplate code review video
  • Import & export MIMEtypes (StackOverflow)


DISCLAIMER: I was a member of various GWS product teams ~2013-2018. While product information is as accurate as I can find or recall, the opinions are my own.



WESLEY CHUN, MSCS, is a Google Developer Expert (GDE) in Google Cloud (GCP) & Google Workspace (GWS), author of Prentice Hall's bestselling "Core Python" series, co-author of "Python Web Development with Django", and has written for Linux Journal & CNET. He runs CyberWeb specializing in GCP & GWS APIs and serverless platforms, Python & App Engine migrations, and Python training & engineering. Wesley was one of the original Yahoo!Mail engineers and spent 13+ years on various Google product teams, speaking on behalf of their APIs, producing sample apps, codelabs, and videos for serverless migration and GWS developers. He holds degrees in Computer Science, Mathematics, and Music from the University of California, is a Fellow of the Python Software Foundation, and loves to travel to meet developers worldwide at conferences, user group events, and universities. Follow he/him @wescpy & his technical blog. Find this content useful? Contact CyberWeb for professional services or buy him a coffee (or tea)!

Top comments (2)

Collapse
 
krowin profile image
KRowin

How about?:
CTRL+C
Alt+Tab from md to Google Docs
CTRL+V

Collapse
 
wescpy profile image
Wesley Chun (@wescpy)

This only works if you use a WYSIWYG Markdown editor and have enabled Markdown in Google Docs. (It didn't work [paste correctly] for me with a plain text code editor.) More importantly, while this is a cute trick that you can do once, twice, or several times a day, would you really want to use the UI and do this for 100s or 1000s of MD documents? As suggested in my post, for a massive amount of data, you really need to use APIs and do this type of processing automatically, programmatically. Thanks for the suggestion however; your solution will indeed work for some.