diff --git a/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/TableCell.java b/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/TableCell.java
index 61880c6c377ec2382fa337e6d464605ac33468b5..bd7b40e02154ec99d4efd8138facd7998cd487cf 100644
--- a/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/TableCell.java
+++ b/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/TableCell.java
@@ -22,7 +22,7 @@ public class TableCell extends CustomNode {
     }
 
     /**
-     * @return the cell alignment
+     * @return the cell alignment or {@code null} if no specific alignment
      */
     public Alignment getAlignment() {
         return alignment;
diff --git a/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/TablesExtension.java b/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/TablesExtension.java
index 5707b0f1406f24b893aa8e08ca2ea754869b2260..d23f6f5fc35bb6bfaa86044ded3510dd0c15550f 100644
--- a/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/TablesExtension.java
+++ b/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/TablesExtension.java
@@ -3,12 +3,16 @@ package org.commonmark.ext.gfm.tables;
 import org.commonmark.Extension;
 import org.commonmark.ext.gfm.tables.internal.TableBlockParser;
 import org.commonmark.ext.gfm.tables.internal.TableHtmlNodeRenderer;
+import org.commonmark.ext.gfm.tables.internal.TableMarkdownNodeRenderer;
 import org.commonmark.ext.gfm.tables.internal.TableTextContentNodeRenderer;
 import org.commonmark.parser.Parser;
 import org.commonmark.renderer.NodeRenderer;
 import org.commonmark.renderer.html.HtmlNodeRendererContext;
 import org.commonmark.renderer.html.HtmlNodeRendererFactory;
 import org.commonmark.renderer.html.HtmlRenderer;
+import org.commonmark.renderer.markdown.MarkdownNodeRendererContext;
+import org.commonmark.renderer.markdown.MarkdownNodeRendererFactory;
+import org.commonmark.renderer.markdown.MarkdownRenderer;
 import org.commonmark.renderer.text.TextContentNodeRendererContext;
 import org.commonmark.renderer.text.TextContentNodeRendererFactory;
 import org.commonmark.renderer.text.TextContentRenderer;
@@ -27,7 +31,7 @@ import org.commonmark.renderer.text.TextContentRenderer;
  * @see <a href="https://github.github.com/gfm/#tables-extension-">Tables (extension) in GitHub Flavored Markdown Spec</a>
  */
 public class TablesExtension implements Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension,
-        TextContentRenderer.TextContentRendererExtension {
+        TextContentRenderer.TextContentRendererExtension, MarkdownRenderer.MarkdownRendererExtension {
 
     private TablesExtension() {
     }
@@ -60,4 +64,14 @@ public class TablesExtension implements Parser.ParserExtension, HtmlRenderer.Htm
             }
         });
     }
+
+    @Override
+    public void extend(MarkdownRenderer.Builder rendererBuilder) {
+        rendererBuilder.nodeRendererFactory(new MarkdownNodeRendererFactory() {
+            @Override
+            public NodeRenderer create(MarkdownNodeRendererContext context) {
+                return new TableMarkdownNodeRenderer(context);
+            }
+        });
+    }
 }
diff --git a/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableMarkdownNodeRenderer.java b/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableMarkdownNodeRenderer.java
new file mode 100644
index 0000000000000000000000000000000000000000..bf94db5bbd51b1a464c08b178bd588cf64dd1e17
--- /dev/null
+++ b/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableMarkdownNodeRenderer.java
@@ -0,0 +1,96 @@
+package org.commonmark.ext.gfm.tables.internal;
+
+import org.commonmark.ext.gfm.tables.*;
+import org.commonmark.internal.util.AsciiMatcher;
+import org.commonmark.internal.util.CharMatcher;
+import org.commonmark.node.Node;
+import org.commonmark.renderer.NodeRenderer;
+import org.commonmark.renderer.markdown.MarkdownNodeRendererContext;
+import org.commonmark.renderer.markdown.MarkdownWriter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The Table node renderer that is needed for rendering GFM tables (GitHub Flavored Markdown) to text content.
+ */
+public class TableMarkdownNodeRenderer extends TableNodeRenderer implements NodeRenderer {
+    private final MarkdownWriter writer;
+    private final MarkdownNodeRendererContext context;
+
+    private final CharMatcher pipe = AsciiMatcher.builder().c('|').build();
+
+    private final List<TableCell.Alignment> columns = new ArrayList<>();
+
+    public TableMarkdownNodeRenderer(MarkdownNodeRendererContext context) {
+        this.writer = context.getWriter();
+        this.context = context;
+    }
+
+    @Override
+    protected void renderBlock(TableBlock node) {
+        columns.clear();
+        renderChildren(node);
+        writer.block();
+    }
+
+    @Override
+    protected void renderHead(TableHead node) {
+        renderChildren(node);
+        // TODO: Not sure about this.. Should block() detect if a line was already written? Or should line() itself be lazy?
+        writer.line();
+        for (TableCell.Alignment columnAlignment : columns) {
+            writer.raw('|');
+            if (columnAlignment == TableCell.Alignment.LEFT) {
+                writer.raw(":---");
+            } else if (columnAlignment == TableCell.Alignment.RIGHT) {
+                writer.raw("---:");
+            } else if (columnAlignment == TableCell.Alignment.CENTER) {
+                writer.raw(":---:");
+            } else {
+                writer.raw("---");
+            }
+        }
+        writer.raw("|");
+        // TODO
+        if (node.getNext() != null) {
+            writer.line();
+        }
+    }
+
+    @Override
+    protected void renderBody(TableBody node) {
+        renderChildren(node);
+    }
+
+    @Override
+    protected void renderRow(TableRow node) {
+        renderChildren(node);
+        // Trailing | at the end of the line
+        writer.raw("|");
+        // TODO
+        if (node.getNext() != null) {
+            writer.line();
+        }
+    }
+
+    @Override
+    protected void renderCell(TableCell node) {
+        if (node.getParent() != null && node.getParent().getParent() instanceof TableHead) {
+            columns.add(node.getAlignment());
+        }
+        writer.raw("|");
+        writer.pushRawEscape(pipe);
+        renderChildren(node);
+        writer.popRawEscape();
+    }
+
+    private void renderChildren(Node parent) {
+        Node node = parent.getFirstChild();
+        while (node != null) {
+            Node next = node.getNext();
+            context.render(node);
+            node = next;
+        }
+    }
+}
diff --git a/commonmark-ext-gfm-tables/src/test/java/org/commonmark/ext/gfm/tables/TablesMarkdownRenderingTest.java b/commonmark-ext-gfm-tables/src/test/java/org/commonmark/ext/gfm/tables/TablesMarkdownRenderingTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..8689e4ac2f7d1622c2b94caeaad72b4cfccbf56c
--- /dev/null
+++ b/commonmark-ext-gfm-tables/src/test/java/org/commonmark/ext/gfm/tables/TablesMarkdownRenderingTest.java
@@ -0,0 +1,70 @@
+package org.commonmark.ext.gfm.tables;
+
+import org.commonmark.Extension;
+import org.commonmark.parser.Parser;
+import org.commonmark.renderer.markdown.MarkdownRenderer;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+
+public class TablesMarkdownRenderingTest {
+
+    private static final Set<Extension> EXTENSIONS = Collections.singleton(TablesExtension.create());
+    private static final Parser PARSER = Parser.builder().extensions(EXTENSIONS).build();
+    private static final MarkdownRenderer RENDERER = MarkdownRenderer.builder().extensions(EXTENSIONS).build();
+
+    @Test
+    public void testHeadNoBody() {
+        assertRoundTrip("|Abc|\n|---|\n");
+        assertRoundTrip("|Abc|Def|\n|---|---|\n");
+        assertRoundTrip("|Abc||\n|---|---|\n");
+    }
+
+    @Test
+    public void testHeadAndBody() {
+        assertRoundTrip("|Abc|\n|---|\n|1|\n");
+        assertRoundTrip("|Abc|Def|\n|---|---|\n|1|2|\n");
+    }
+
+    @Test
+    public void testBodyHasFewerColumns() {
+        // Could try not to write empty trailing cells but this is fine too
+        assertRoundTrip("|Abc|Def|\n|---|---|\n|1||\n");
+    }
+
+    @Test
+    public void testAlignment() {
+        assertRoundTrip("|Abc|Def|\n|:---|---|\n|1|2|\n");
+        assertRoundTrip("|Abc|Def|\n|---|---:|\n|1|2|\n");
+        assertRoundTrip("|Abc|Def|\n|:---:|:---:|\n|1|2|\n");
+    }
+
+    @Test
+    public void testInsideBlockQuote() {
+        assertRoundTrip("> |Abc|Def|\n> |---|---|\n> |1|2|\n");
+    }
+
+    @Test
+    public void testMultipleTables() {
+        assertRoundTrip("|Abc|Def|\n|---|---|\n\n|One|\n|---|\n|Only|\n");
+    }
+
+    @Test
+    public void testEscaping() {
+        assertRoundTrip("|Abc|Def|\n|---|---|\n|Pipe in|text \\||\n");
+        assertRoundTrip("|Abc|Def|\n|---|---|\n|Pipe in|code `\\|`|\n");
+        assertRoundTrip("|Abc|Def|\n|---|---|\n|Inline HTML|<span>Foo\\|bar</span>|\n");
+    }
+
+    protected String render(String source) {
+        return RENDERER.render(PARSER.parse(source));
+    }
+
+    private void assertRoundTrip(String input) {
+        String rendered = render(input);
+        assertEquals(input, rendered);
+    }
+}
diff --git a/commonmark/src/main/java/org/commonmark/renderer/markdown/CoreMarkdownNodeRenderer.java b/commonmark/src/main/java/org/commonmark/renderer/markdown/CoreMarkdownNodeRenderer.java
index f5a9377f6626145574f1ab7af31b6969a53aed8d..714144e89e22dce2d313bdd7f511a831cfd4f040 100644
--- a/commonmark/src/main/java/org/commonmark/renderer/markdown/CoreMarkdownNodeRenderer.java
+++ b/commonmark/src/main/java/org/commonmark/renderer/markdown/CoreMarkdownNodeRenderer.java
@@ -23,15 +23,15 @@ import java.util.regex.Pattern;
 public class CoreMarkdownNodeRenderer extends AbstractVisitor implements NodeRenderer {
 
     private final AsciiMatcher textEscape =
-            AsciiMatcher.builder().anyOf("[]<>`*&").build();
+            AsciiMatcher.builder().anyOf("[]<>`*&\n\\").build();
     private final CharMatcher textEscapeInHeading =
             AsciiMatcher.builder(textEscape).anyOf("#").build();
     private final CharMatcher linkDestinationNeedsAngleBrackets =
-            AsciiMatcher.builder().c(' ').c('(').c(')').c('<').c('>').c('\\').build();
+            AsciiMatcher.builder().c(' ').c('(').c(')').c('<').c('>').c('\n').c('\\').build();
     private final CharMatcher linkDestinationEscapeInAngleBrackets =
-            AsciiMatcher.builder().c('<').c('>').build();
+            AsciiMatcher.builder().c('<').c('>').c('\n').c('\\').build();
     private final CharMatcher linkTitleEscapeInQuotes =
-            AsciiMatcher.builder().c('"').build();
+            AsciiMatcher.builder().c('"').c('\n').c('\\').build();
 
     private final Pattern orderedListMarkerPattern = Pattern.compile("^([0-9]{1,9})([.)])");
 
diff --git a/commonmark/src/main/java/org/commonmark/renderer/markdown/MarkdownWriter.java b/commonmark/src/main/java/org/commonmark/renderer/markdown/MarkdownWriter.java
index 3b2ae18b02ab6a3830bfd69e156dd269cc06242e..f648d9c4ff30dbda8a38ff8ae69a5a84cbfc9818 100644
--- a/commonmark/src/main/java/org/commonmark/renderer/markdown/MarkdownWriter.java
+++ b/commonmark/src/main/java/org/commonmark/renderer/markdown/MarkdownWriter.java
@@ -17,25 +17,26 @@ public class MarkdownWriter {
     private char lastChar;
     private boolean atLineStart = true;
     private final LinkedList<String> prefixes = new LinkedList<>();
+    private final LinkedList<CharMatcher> rawEscapes = new LinkedList<>();
 
     public MarkdownWriter(Appendable out) {
         buffer = out;
     }
 
     /**
-     * Write the supplied string (raw/unescaped).
+     * Write the supplied string (raw/unescaped except if {@link #pushRawEscape} was used).
      */
     public void raw(String s) {
         flushBlockSeparator();
-        append(s);
+        write(s, null);
     }
 
     /**
-     * Write the supplied character (raw/unescaped).
+     * Write the supplied character (raw/unescaped except if {@link #pushRawEscape} was used).
      */
     public void raw(char c) {
         flushBlockSeparator();
-        append(c);
+        write(c);
     }
 
     /**
@@ -49,22 +50,7 @@ public class MarkdownWriter {
             return;
         }
         flushBlockSeparator();
-        try {
-            for (int i = 0; i < s.length(); i++) {
-                char ch = s.charAt(i);
-                if (ch == '\n') {
-                    // Can't escape this with \, use numeric character reference
-                    buffer.append("&#10;");
-                } else {
-                    if (ch == '\\' || escape.matches(ch)) {
-                        buffer.append('\\');
-                    }
-                    buffer.append(ch);
-                }
-            }
-        } catch (IOException e) {
-            throw new RuntimeException(e);
-        }
+        write(s, escape);
 
         lastChar = s.charAt(s.length() - 1);
         atLineStart = false;
@@ -74,7 +60,7 @@ public class MarkdownWriter {
      * Write a newline (line terminator).
      */
     public void line() {
-        append('\n');
+        write('\n');
         writePrefixes();
         atLineStart = true;
     }
@@ -118,6 +104,24 @@ public class MarkdownWriter {
         prefixes.removeLast();
     }
 
+    /**
+     * Escape the characters matching the supplied matcher, in all text (text and raw). This might be useful to
+     * extensions that add another layer of syntax, e.g. the tables extension that uses `|` to separate cells and needs
+     * all `|` characters to be escaped (even in code spans).
+     *
+     * @param rawEscape the characters to escape in raw text
+     */
+    public void pushRawEscape(CharMatcher rawEscape) {
+        rawEscapes.add(rawEscape);
+    }
+
+    /**
+     * Remove the last raw escape from the top of the stack.
+     */
+    public void popRawEscape() {
+        rawEscapes.removeLast();
+    }
+
     /**
      * @return the last character that was written
      */
@@ -151,9 +155,16 @@ public class MarkdownWriter {
         this.tight = tight;
     }
 
-    private void append(String s) {
+    private void write(String s, CharMatcher escape) {
         try {
-            buffer.append(s);
+            if (rawEscapes.isEmpty() && escape == null) {
+                // Normal fast path
+                buffer.append(s);
+            } else {
+                for (int i = 0; i < s.length(); i++) {
+                    append(s.charAt(i), escape);
+                }
+            }
         } catch (IOException e) {
             throw new RuntimeException(e);
         }
@@ -165,9 +176,9 @@ public class MarkdownWriter {
         atLineStart = false;
     }
 
-    private void append(char c) {
+    private void write(char c) {
         try {
-            buffer.append(c);
+            append(c, null);
         } catch (IOException e) {
             throw new RuntimeException(e);
         }
@@ -179,7 +190,7 @@ public class MarkdownWriter {
     private void writePrefixes() {
         if (!prefixes.isEmpty()) {
             for (String prefix : prefixes) {
-                append(prefix);
+                write(prefix, null);
             }
         }
     }
@@ -189,13 +200,40 @@ public class MarkdownWriter {
      */
     private void flushBlockSeparator() {
         if (blockSeparator != 0) {
-            append('\n');
+            write('\n');
             writePrefixes();
             if (blockSeparator > 1) {
-                append('\n');
+                write('\n');
                 writePrefixes();
             }
             blockSeparator = 0;
         }
     }
+
+    private void append(char c, CharMatcher escape) throws IOException {
+        if (needsEscaping(c, escape)) {
+            if (c == '\n') {
+                // Can't escape this with \, use numeric character reference
+                buffer.append("&#10;");
+            } else {
+                buffer.append('\\');
+                buffer.append(c);
+            }
+        } else {
+            buffer.append(c);
+        }
+    }
+
+    private boolean needsEscaping(char c, CharMatcher escape) {
+        return (escape != null && escape.matches(c)) || rawNeedsEscaping(c);
+    }
+
+    private boolean rawNeedsEscaping(char c) {
+        for (CharMatcher rawEscape : rawEscapes) {
+            if (rawEscape.matches(c)) {
+                return true;
+            }
+        }
+        return false;
+    }
 }
diff --git a/commonmark/src/test/java/org/commonmark/renderer/markdown/MarkdownRendererTest.java b/commonmark/src/test/java/org/commonmark/renderer/markdown/MarkdownRendererTest.java
index bc6d9b696b0f5b3e3a3b1667b96899a89e130571..20453eed7fb5ca6a6f9be4260120cea9f9119feb 100644
--- a/commonmark/src/test/java/org/commonmark/renderer/markdown/MarkdownRendererTest.java
+++ b/commonmark/src/test/java/org/commonmark/renderer/markdown/MarkdownRendererTest.java
@@ -194,6 +194,9 @@ public class MarkdownRendererTest {
         assertRoundTrip("[a](<b\\>c>)\n");
         assertRoundTrip("[a](<b\\\\\\>c>)\n");
         assertRoundTrip("[a](/uri \"foo \\\" bar\")\n");
+        assertRoundTrip("[link](/uri \"tes\\\\\")\n");
+        assertRoundTrip("[link](/url \"test&#10;&#10;\")\n");
+        assertRoundTrip("[link](</url&#10;&#10;>)\n");
     }
 
     @Test